How to apply multiple predicates to a java.util.Stream?

有些话、适合烂在心里 提交于 2019-11-26 09:18:55

问题


How can I apply multiple predicates to a java.util.Stream\'s filter() method?

This is what I do now, but I don\'t really like it. I have a Collection of things and I need to reduce the number of things based on the Collection of filters (predicates):

Collection<Thing> things = someGenerator.someMethod();
List<Thing> filtered = things.parallelStream().filter(p -> {
   for (Filter f : filtersCollection) {
      if (f.test(p))
        return true;
   }
   return false;
}).collect(Collectors.toList());

I know that if I knew number of filters up-front, I could do something like this:

List<Thing> filtered = things.parallelStream().filter(filter1).or(filter2).or(filter3)).collect(Collectors.toList());

But how can I apply unknown number of predicates without mixing programming styles? For know it looks sort of ugly...


回答1:


I am assuming your Filter is a type distinct from java.util.function.Predicate, which means it needs to be adapted to it. One approach which will work goes like this:

things.stream().filter(t -> filtersCollection.stream().anyMatch(f -> f.test(t)));

This incurs a slight performance hit of recreating the filter stream for each predicate evaluation. To avoid that you could wrap each filter into a Predicate and compose them:

things.stream().filter(filtersCollection.stream().<Predicate>map(f -> f::test)
                       .reduce(Predicate::or).orElse(t->false));

However, since now each filter is behind its own Predicate, introducing one more level of indirection, it is not clear-cut which approach would have better overall performance.

Without the adapting concern (if your Filter happens to be a Predicate) the problem statement becomes much simpler and the second approach clearly wins out:

things.stream().filter(
   filtersCollection.stream().reduce(Predicate::or).orElse(t->true)
);



回答2:


If you have a Collection<Predicate<T>> filters you can always create a single predicate out of it using the process called reduction:

Predicate<T> pred=filters.stream().reduce(Predicate::and).orElse(x->true);

or

Predicate<T> pred=filters.stream().reduce(Predicate::or).orElse(x->false);

depending on how you want to combine the filters.

If the fallback for an empty predicate collection specified in the orElse call fulfills the identity role (which x->true does for anding the predicates and x->false does for oring) you could also use reduce(x->true, Predicate::and) or reduce(x->false, Predicate::or) to get the filter but that’s slightly less efficient for very small collections as it would always combine the identity predicate with the collection’s predicate even if it contains only one predicate. In contrast, the variant reduce(accumulator).orElse(fallback) shown above will return the single predicate if the collection has size 1.


Note how this pattern applies to similar problems as well: Having a Collection<Consumer<T>> you can create a single Consumer<T> using

Consumer<T> c=consumers.stream().reduce(Consumer::andThen).orElse(x->{});

Etc.




回答3:


I've managed to solve such a problem, if a user wants to apply a list of predicates in one filter operation, a list which can be dynamic and not given, that should be reduced into one predicate - like this:

public class TestPredicates {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        System.out.println(numbers.stream()
                .filter(combineFilters(x -> x > 2, x -> x < 9, x -> x % 2 == 1))
                .collect(Collectors.toList()));
    }

    public static <T> Predicate<T> combineFilters(Predicate<T>... predicates) {

        Predicate<T> p = Stream.of(predicates).reduce(x -> true, Predicate::and);
        return p;

    }
}

Note that this will combine them with an "AND" logic operator. To combine with an "OR" the reduce line should be:

Predicate<T> p = Stream.of(predicates).reduce(x -> false, Predicate::or);



回答4:


This is an interesting way of solving this problem, (directly pasting from http://www.leveluplunch.com/java/tutorials/006-how-to-filter-arraylist-stream-java8/). I think this is a more efficient way.

Predicate<BBTeam> nonNullPredicate = Objects::nonNull;
Predicate<BBTeam> nameNotNull = p -> p.teamName != null;
Predicate<BBTeam> teamWIPredicate = p -> p.teamName.equals("Wisconsin");

Predicate<BBTeam> fullPredicate = nonNullPredicate.and(nameNotNull)
        .and(teamWIPredicate);

List<BBTeam> teams2 = teams.stream().filter(fullPredicate)
        .collect(Collectors.toList());

EDIT: Here's how to deal with loops where predicatesToIgnore is a List of predicates. I create a single predicate predicateToIgnore from it.

Predicate<T> predicateToIgnore = null;
for (Predicate<T> predicate : predicatesToIgnore) {
    predicateToIgnore = predicateToIgnore == null ? predicate : predicateToIgnore.or(predicate);
}

Then, do a filter with this single predicate. This creates a better filter IMHO



来源:https://stackoverflow.com/questions/24553761/how-to-apply-multiple-predicates-to-a-java-util-stream

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!