Hoe kan ik meerdere predikaten toepassen op een java.util.Stream's
filter()
methode?
Dit is wat ik nu doe, maar ik vind het niet echt leuk. Ik heb een Collection
van dingen en ik moet het aantal dingen verminderen op basis van de Collection
van filters (predikaten):
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());
Ik weet dat als ik vooraf het aantal filters wist, ik zoiets als dit zou kunnen doen:
List<Thing> filtered = things.parallelStream().filter(filter1).or(filter2).or(filter3)).collect(Collectors.toList());
Maar hoe kan ik een onbekend aantal predikaten toepassen zonder programmeerstijlen te mengen? Het ziet er namelijk nogal lelijk uit…
Antwoord 1, autoriteit 100%
Als je een Collection<Predicate<T>> filters
je kunt er altijd een enkel predikaat van maken met behulp van het proces genaamd reductie:
Predicate<T> pred=filters.stream().reduce(Predicate::and).orElse(x->true);
of
Predicate<T> pred=filters.stream().reduce(Predicate::or).orElse(x->false);
afhankelijk van hoe je de filters wilt combineren.
Als de fallback voor een lege predikaatverzameling gespecificeerd in de orElse
-aanroep de identiteitsrol vervult (wat x->true
doet voor and
als je de predikaten en x->false
doet voor or
ing) kun je ook reduce(x->true, Predicate::and)
of reduce(x->false, Predicate::or)
om het filter te krijgen, maar dat is iets minder efficiënt voor zeer kleine collecties omdat het altijd het identiteitspredikaat zou combineren met het predikaat van de collectie, zelfs als het slechts één predikaat bevat. De variant reduce(accumulator).orElse(fallback)
die hierboven wordt weergegeven, retourneert daarentegen het enkele predikaat als de verzameling de grootte 1
heeft.
Merk op hoe dit patroon ook van toepassing is op soortgelijke problemen: met een Collection<Consumer<T>>
kunt u een enkele Consumer<T>
maken met
Consumer<T> c=consumers.stream().reduce(Consumer::andThen).orElse(x->{});
Enz.
Antwoord 2, autoriteit 83%
Ik neem aan dat je Filter
een ander type is dan java.util.function.Predicate
, wat betekent dat het eraan moet worden aangepast. Een benadering die zal werken, gaat als volgt:
things.stream().filter(t -> filtersCollection.stream().anyMatch(f -> f.test(t)));
Dit leidt tot een kleine prestatiefout bij het opnieuw maken van de filterstroom voor elke predikaatevaluatie. Om dat te voorkomen zou je elk filter in een Predicate
kunnen stoppen en ze samenstellen:
things.stream().filter(filtersCollection.stream().<Predicate>map(f -> f::test)
.reduce(Predicate::or).orElse(t->false));
Omdat nu elk filter echter achter zijn eigen Predicate
zit, waardoor een extra niveau van indirectheid wordt geïntroduceerd, is het niet duidelijk welke benadering betere algehele prestaties zou hebben.
Zonder de zorg voor aanpassing (als uw Filter
toevallig een Predicate
is) wordt de probleemstelling veel eenvoudiger en wint de tweede benadering duidelijk:
things.stream().filter(
filtersCollection.stream().reduce(Predicate::or).orElse(t->true)
);
Antwoord 3, autoriteit 22%
Ik ben erin geslaagd om een dergelijk probleem op te lossen, als een gebruiker een lijst met predikaten in één filterbewerking wil toepassen, een lijst die dynamisch kan zijn en niet kan worden gegeven, moet die worden teruggebracht tot één predikaat – zoals dit:
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;
}
}
Merk op dat dit ze combineert met een “AND” logische operator.
Om te combineren met een “OF” moet de verkleiningslijn zijn:
Predicate<T> p = Stream.of(predicates).reduce(x -> false, Predicate::or);
Antwoord 4, autoriteit 14%
Dit is een interessante manier om dit probleem op te lossen (direct plakken vanuit http://www.leveluplunch.com/java/tutorials/006-how-to-filter-arraylist-stream-java8/). Ik denk dat dit een efficiëntere manier is.
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: Hier leest u hoe u omgaat met lussen waarbij predicatesToIgnore een lijst met predikaten is. Ik maak er een enkel predikaat predicateToIgnore van.
Predicate<T> predicateToIgnore = null;
for (Predicate<T> predicate : predicatesToIgnore) {
predicateToIgnore = predicateToIgnore == null ? predicate : predicateToIgnore.or(predicate);
}
Voer vervolgens een filter uit met dit enkele predikaat. Dit zorgt voor een beter filter IMHO