web-dev-qa-db-fra.com

Comment faire un filtrage dynamique dans Java 8?

Je sais en Java 8, je peux faire un filtrage comme ceci:

List<User> olderUsers = users.stream().filter(u -> u.age > 30).collect(Collectors.toList());

Mais que se passe-t-il si j'ai une collection et une demi-douzaine de critères de filtrage et que je veux tester la combinaison des critères?

Par exemple, j'ai une collection d'objets et les critères suivants:

<1> Size
<2> Weight
<3> Length
<4> Top 50% by a certain order
<5> Top 20% by a another certain ratio
<6> True or false by yet another criteria

Et je veux tester la combinaison des critères ci-dessus, quelque chose comme:

<1> -> <2> -> <3> -> <4> -> <5>
<1> -> <2> -> <3> -> <5> -> <4>
<1> -> <2> -> <5> -> <4> -> <3>
...
<1> -> <5> -> <3> -> <4> -> <2>
<3> -> <2> -> <1> -> <4> -> <5>
...
<5> -> <4> -> <3> -> <3> -> <1>

Si chaque ordre de test peut me donner des résultats différents, comment écrire une boucle pour filtrer automatiquement toutes les combinaisons?

Ce à quoi je peux penser, c'est d'utiliser une autre méthode qui génère l'ordre de test comme suit:

int[][] getTestOrder(int criteriaCount)
{
 ...
}

So if the criteriaCount is 2, it will return : {{1,2},{2,1}}
If the criteriaCount is 3, it will return : {{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}}
...

Mais alors comment l'implémenter le plus efficacement possible avec le mécanisme de filtrage dans des expressions concises fournies avec Java 8?

36
Frank

Problème intéressant. Il se passe plusieurs choses ici. Nul doute que cela pourrait être résolu en moins d'une demi-page de Haskell ou LISP, mais c'est Java, alors c'est parti ...

Un problème est que nous avons un nombre variable de filtres, alors que la plupart des exemples qui ont été illustrés illustrent des pipelines fixes.

Un autre problème est que certains des "filtres" du PO sont sensibles au contexte, tels que "les 50% supérieurs selon un certain ordre". Cela ne peut pas être fait avec une simple construction filter(predicate) sur un flux.

La clé est de réaliser que, si les lambdas permettent de passer des fonctions en arguments (à bon escient), cela signifie également qu'elles peuvent être stockées dans des structures de données et que des calculs peuvent y être effectués. Le calcul le plus courant consiste à prendre plusieurs fonctions et à les composer.

Supposons que les valeurs utilisées sont des instances de Widget, qui est un POJO qui a des getters évidents:

class Widget {
    String name() { ... }
    int length() { ... }
    double weight() { ... }

    // constructors, fields, toString(), etc.
}

Commençons par le premier problème et voyons comment fonctionner avec un nombre variable de prédicats simples. Nous pouvons créer une liste de prédicats comme ceci:

List<Predicate<Widget>> allPredicates = Arrays.asList(
    w -> w.length() >= 10,
    w -> w.weight() > 40.0,
    w -> w.name().compareTo("c") > 0);

Compte tenu de cette liste, nous pouvons les permuter (probablement pas utiles, car ils sont indépendants de l'ordre) ou sélectionner n'importe quel sous-ensemble que nous voulons. Disons que nous voulons simplement les appliquer tous. Comment appliquer un nombre variable de prédicats à un flux? Il existe une méthode Predicate.and() qui prendra deux prédicats et les combinera en utilisant un logique et , retournant un seul prédicat. Nous pourrions donc prendre le premier prédicat et écrire une boucle qui le combine avec les prédicats successifs pour créer un seul prédicat composé et de tous:

Predicate<Widget> compositePredicate = allPredicates.get(0);
for (int i = 1; i < allPredicates.size(); i++) {
    compositePredicate = compositePredicate.and(allPredicates.get(i));
}

Cela fonctionne, mais il échoue si la liste est vide, et puisque nous faisons de la programmation fonctionnelle maintenant, la mutation d'une variable dans une boucle est déclassée. Mais voilà! C'est une réduction! Nous pouvons réduire tous les prédicats sur l'opérateur et obtenir un seul prédicat composite, comme ceci:

Predicate<Widget> compositePredicate =
    allPredicates.stream()
                 .reduce(w -> true, Predicate::and);

(Crédit: j'ai appris cette technique de @ venkat_s . Si jamais vous en avez l'occasion, allez le voir parler lors d'une conférence. Il est bon.)

Notez l'utilisation de w -> true Comme valeur d'identité de la réduction. (Cela pourrait également être utilisé comme valeur initiale de compositePredicate pour la boucle, ce qui corrigerait le cas de liste de longueur nulle.)

Maintenant que nous avons notre prédicat composite, nous pouvons écrire un court pipeline qui applique simplement le prédicat composite aux widgets:

widgetList.stream()
          .filter(compositePredicate)
          .forEach(System.out::println);

Filtres sensibles au contexte

Considérons maintenant ce que j'ai appelé un filtre "contextuel", qui est représenté par l'exemple comme "les 50% supérieurs dans un certain ordre", par exemple les 50% supérieurs de widgets en poids. "Sensible au contexte" n'est pas le meilleur terme pour cela, mais c'est ce que j'ai en ce moment, et il est quelque peu descriptif en ce qu'il est relatif au nombre d'éléments dans le flux jusqu'à ce point.

Comment pourrions-nous implémenter quelque chose comme ça en utilisant des flux? À moins que quelqu'un ne trouve quelque chose de vraiment intelligent, je pense que nous devons d'abord collecter les éléments quelque part (disons dans une liste) avant de pouvoir émettre le premier élément vers la sortie. C'est un peu comme sorted() dans un pipeline qui ne peut pas dire quel est le premier élément à sortir jusqu'à ce qu'il ait lu chaque élément d'entrée et les ait triés.

L'approche simple pour trouver les 50% des widgets en poids, en utilisant des flux, ressemblerait à ceci:

List<Widget> temp =
    list.stream()
        .sorted(comparing(Widget::weight).reversed())
        .collect(toList());
temp.stream()
    .limit((long)(temp.size() * 0.5))
    .forEach(System.out::println);

Ce n'est pas compliqué, mais c'est un peu lourd car nous devons collecter les éléments dans une liste et l'assigner à une variable, afin d'utiliser la taille de la liste dans le calcul à 50%.

Ceci est cependant limitatif dans la mesure où il s'agit d'une représentation "statique" de ce type de filtrage. Comment pourrions-nous enchaîner cela en un flux avec un nombre variable d'éléments (autres filtres ou critères) comme nous l'avons fait avec les prédicats?

Une observation importante est que ce code fait son travail réel entre la consommation d'un flux et l'émission d'un flux. Il se trouve qu'il y a un collecteur au milieu, mais si vous enchaînez un flux à son front et que vous enchaînez des trucs à l'arrière, personne n'est plus sage. En fait, les opérations de pipeline de flux standard comme map et filter prennent chacune un flux en entrée et émettent un flux en sortie. Nous pouvons donc écrire nous-mêmes une fonction un peu comme ceci:

Stream<Widget> top50PercentByWeight(Stream<Widget> stream) {
    List<Widget> temp =
        stream.sorted(comparing(Widget::weight).reversed())
              .collect(toList());
    return temp.stream()
               .limit((long)(temp.size() * 0.5));
}

Un exemple similaire pourrait être de trouver les trois widgets les plus courts:

Stream<Widget> shortestThree(Stream<Widget> stream) {
    return stream.sorted(comparing(Widget::length))
                 .limit(3);
}

Maintenant, nous pouvons écrire quelque chose qui combine ces filtres avec état avec des opérations de flux ordinaires:

shortestThree(
    top50PercentByWeight(
        widgetList.stream()
                  .filter(w -> w.length() >= 10)))
.forEach(System.out::println);

Cela fonctionne, mais c'est un peu moche car il se lit "à l'envers" et à l'envers. La source du flux est widgetList qui est diffusée et filtrée à travers un prédicat ordinaire. Maintenant, en reculant, le filtre des 50% supérieurs est appliqué, puis le filtre des trois plus courts est appliqué, et enfin l'opération de flux forEach est appliquée à la fin. Cela fonctionne mais est assez déroutant à lire. Et c'est toujours statique. Ce que nous voulons vraiment, c'est avoir un moyen de mettre ces nouveaux filtres à l'intérieur d'une structure de données que nous pouvons manipuler, par exemple, pour exécuter toutes les permutations, comme dans la question d'origine.

Un aperçu clé à ce stade est que ces nouveaux types de filtres ne sont vraiment que des fonctions, et nous avons des types d'interfaces fonctionnelles dans Java qui nous permettent de représenter les fonctions comme des objets, de les manipuler, de les stocker dans structures de données, composez-les, etc. Le type d'interface fonctionnelle qui prend un argument d'un certain type et renvoie une valeur du même type est UnaryOperator. L'argument et le type de retour dans ce cas sont Stream<Widget> Si nous devions prendre des références de méthode telles que this::shortestThree Ou this::top50PercentByWeight, Les types des objets résultants seraient

UnaryOperator<Stream<Widget>>

Si nous devions les mettre dans une liste, le type de cette liste serait

List<UnaryOperator<Stream<Widget>>>

Pouah! Trois niveaux de génériques imbriqués, c'est trop pour moi. (Mais Aleksey Shipilev m'a montré une fois du code utilisant quatre niveaux de génériques imbriqués.) La solution pour trop de génériques est de définir notre propre type. Appelons l'une de nos nouvelles choses un critère. Il s'avère qu'il y a peu de valeur à gagner en faisant que notre nouveau type d'interface fonctionnelle soit lié à UnaryOperator, donc notre définition peut simplement être:

@FunctionalInterface
public interface Criterion {
    Stream<Widget> apply(Stream<Widget> s);
}

Maintenant, nous pouvons créer une liste de critères comme celui-ci:

List<Criterion> criteria = Arrays.asList(
    this::shortestThree,
    this::lengthGreaterThan20
);

(Nous allons voir comment utiliser cette liste ci-dessous.) Il s'agit d'un pas en avant, car nous pouvons maintenant manipuler la liste de manière dynamique, mais elle reste quelque peu limitative. Premièrement, il ne peut pas être combiné avec des prédicats ordinaires. Deuxièmement, il y a beaucoup de valeurs codées en dur ici, telles que les trois plus courtes: que diriez-vous de deux ou quatre? Que diriez-vous d'un critère différent de la longueur? Ce que nous voulons vraiment, c'est une fonction qui crée ces objets Criterion pour nous. C'est facile avec les lambdas.

Cela crée un critère qui sélectionne les N meilleurs widgets, étant donné un comparateur:

Criterion topN(Comparator<Widget> cmp, long n) {
    return stream -> stream.sorted(cmp).limit(n);
}

Cela crée un critère qui sélectionne le p pour cent supérieur de widgets, étant donné un comparateur:

Criterion topPercent(Comparator<Widget> cmp, double pct) {
    return stream -> {
        List<Widget> temp =
            stream.sorted(cmp).collect(toList());
        return temp.stream()
                   .limit((long)(temp.size() * pct));
    };
}

Et cela crée un critère à partir d'un prédicat ordinaire:

Criterion fromPredicate(Predicate<Widget> pred) {
    return stream -> stream.filter(pred);
}

Maintenant, nous avons un moyen très flexible de créer des critères et de les mettre dans une liste, où ils peuvent être sous-définis ou permutés ou autre:

List<Criterion> criteria = Arrays.asList(
    fromPredicate(w -> w.length() > 10),                    // longer than 10
    topN(comparing(Widget::length), 4L),                    // longest 4
    topPercent(comparing(Widget::weight).reversed(), 0.50)  // heaviest 50%
);

Une fois que nous avons une liste d'objets Criterion, nous devons trouver un moyen de les appliquer tous. Encore une fois, nous pouvons utiliser notre ami reduce pour les combiner en un seul objet Criterion:

Criterion allCriteria =
    criteria.stream()
            .reduce(c -> c, (c1, c2) -> (s -> c2.apply(c1.apply(s))));

La fonction d'identité c -> c Est claire, mais le deuxième argument est un peu délicat. Étant donné un flux s, nous appliquons d'abord le critère c1, puis le critère c2, et celui-ci est enveloppé dans un lambda qui prend deux objets de critère c1 et c2 et renvoie un lambda qui applique la composition de c1 et c2 à un flux et renvoie le flux résultant.

Maintenant que nous avons composé tous les critères, nous pouvons l'appliquer à un flux de widgets comme ceci:

allCriteria.apply(widgetList.stream())
           .forEach(System.out::println);

C'est encore un peu à l'envers, mais c'est assez bien contrôlé. Plus important encore, il répond à la question initiale, à savoir comment combiner dynamiquement les critères. Une fois que les objets Criterion sont dans une structure de données, ils peuvent être sélectionnés, sous-paramétrés, permutés, ou tout ce qui est nécessaire, et ils peuvent tous être combinés en un seul critère et appliqués à un flux en utilisant les techniques ci-dessus.

Les gourous de la programmation fonctionnelle disent probablement "Il vient de réinventer ...!" ce qui est probablement vrai. Je suis sûr que cela a probablement déjà été inventé quelque part, mais c'est nouveau pour Java, car avant lambda, il n'était tout simplement pas possible d'écrire Java code qui utilise ces techniques.

Mise à jour 2014-04-07

J'ai nettoyé et publié l'intégralité de exemple de code dans un Gist.

78
Stuart Marks

Nous pourrions ajouter un compteur avec une carte afin que nous sachions combien d'éléments nous avons après les filtres. J'ai créé une classe d'assistance qui a une méthode qui compte et renvoie le même objet transmis:

class DoNothingButCount<T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T pass(T p) {
        i.incrementAndGet();
        return p;
    }
}

public void runDemo() {
    List<Person>persons = create(100);
    DoNothingButCount<Person> counter = new DoNothingButCount<>();

    persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
            map((p) -> counter.pass(p)).
            sorted((p1, p2) -> p1.age - p2.age).
            collect(Collectors.toList()).stream().
            limit((int) (counter.i.intValue() * 0.5)).
            sorted((p1, p2) -> p2.length - p1.length).
            limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
}

J'ai dû convertir le flux en liste et revenir en flux au milieu parce que la limite utiliserait le nombre initial sinon. C'est tout sauf un "hackish" mais c'est tout ce que je pouvais penser.

Je pourrais le faire un peu différemment en utilisant une fonction pour ma classe mappée:

class DoNothingButCount<T > implements Function<T, T> {
    AtomicInteger i;
    public DoNothingButCount() {
        i = new AtomicInteger(0);
    }
    public T apply(T p) {
        i.incrementAndGet();
        return p;
    }
}

La seule chose qui changera dans le flux est:

            map((p) -> counter.pass(p)).

va devenir:

            map(counter).

Ma classe de test complète comprenant les deux exemples:

import Java.util.*;
import Java.util.concurrent.atomic.AtomicInteger;
import Java.util.function.Function;
import Java.util.stream.Collectors;

public class Demo2 {
    Random r = new Random();
    class Person {
        public int size, weitght,length, age;
        public Person(int s, int w, int l, int a){
            this.size = s;
            this.weitght = w;
            this.length = l;
            this.age = a;
        }
        public String toString() {
            return "P: "+this.size+", "+this.weitght+", "+this.length+", "+this.age+".";
        }
    }

    public List<Person>create(int size) {
        List<Person>persons = new ArrayList<>();
        while(persons.size()<size) {
            persons.add(new Person(r.nextInt(10)+10, r.nextInt(10)+10, r.nextInt(10)+10,r.nextInt(20)+14));
        }
        return persons;
    }

    class DoNothingButCount<T> {
        AtomicInteger i;
        public DoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T pass(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    class PDoNothingButCount<T > implements Function<T, T> {
        AtomicInteger i;
        public PDoNothingButCount() {
            i = new AtomicInteger(0);
        }
        public T apply(T p) {
            i.incrementAndGet();
            return p;
        }
    }

    public void runDemo() {
        List<Person>persons = create(100);
        PDoNothingButCount<Person> counter = new PDoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map(counter).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public void runDemo2() {
        List<Person>persons = create(100);
        DoNothingButCount<Person> counter = new DoNothingButCount<>();

        persons.stream().filter(u -> u.size > 12).filter(u -> u.weitght > 12).
                map((p) -> counter.pass(p)).
                sorted((p1, p2) -> p1.age - p2.age).
                collect(Collectors.toList()).stream().
                limit((int) (counter.i.intValue() * 0.5)).
                sorted((p1, p2) -> p2.length - p1.length).
                limit((int) (counter.i.intValue() * 0.5 * 0.2)).forEach((p) -> System.out.println(p));
    }

    public static void main(String str[]) {
        Demo2 demo = new Demo2();
        System.out.println("Demo 2:");
        demo.runDemo2();
        System.out.println("Demo 1:");
        demo.runDemo();

    }
}
2
Raul Guiu