Hoe pas ik meerdere predikaten toe op een java.util.Stream?

Hoe kan ik meerdere predikaten toepassen op een java.util.Stream'sfilter()methode?

Dit is wat ik nu doe, maar ik vind het niet echt leuk. Ik heb een Collectionvan dingen en ik moet het aantal dingen verminderen op basis van de Collectionvan 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>> filtersje 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->truedoet voor andals je de predikaten en x->falsedoet voor oring) 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 1heeft.


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 Filtereen 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 Predicatekunnen 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 Predicatezit, 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 Filtertoevallig een Predicateis) 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

Other episodes