une Comment faire du filtrage dynamique en Java 8?




recherche dans une base de données mysql php (2)

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

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));
}

Je devais convertir le flux en liste et le rediffuser au milieu car la limite utilisait le décompte initial sinon. Est-ce tout 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 incluant 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();

    }
}

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

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

Mais que faire si j'ai une collection et une demi-douzaine de critères de filtrage, et 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 donner des résultats différents, comment écrire une boucle pour filtrer automatiquement toutes les combinaisons?

Ce que je peux penser 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 avec le mécanisme de filtrage dans des expressions concises fournies avec Java 8?


Problème intéressant Il y a plusieurs choses qui se passent ici. Nul doute que cela pourrait être résolu en moins d’une demi-page de Haskell ou Lisp, mais c’est Java, alors on y va ....

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

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

La clé est de réaliser que, bien que les lambdas permettent aux fonctions d'être passées en argument (à bon escient), cela signifie également qu'elles peuvent être stockées dans des structures de données et que des calculs peuvent être effectués sur elles. 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 objet POJO qui comporte des obstacles évidents:

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

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

Commençons par le premier numéro et trouvons comment opérer 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 utile, car ils sont indépendants de la commande) ou sélectionner n'importe quel sous-ensemble que nous voulons. Disons que nous voulons juste les appliquer tous. Comment appliquer un nombre variable de prédicats à un flux? Il existe une méthode Predicate.and() qui prend deux prédicats et les combine en utilisant un logique et en renvoyant 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 construire un prédicat unique qui soit un composite et tous:

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

Cela fonctionne, mais cela échoue si la liste est vide, et comme nous faisons maintenant de la programmation fonctionnelle, 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 avec @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 permettrait de corriger 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 "sensible au contexte", représenté par l'exemple "top 50% dans un certain ordre", par exemple les 50% supérieurs des widgets en poids. "Context sensible" n'est pas le meilleur terme pour cela, mais c'est ce que j'ai pour le moment, et il est quelque peu descriptif en ce sens qu'il est relatif au nombre d'éléments dans le flux jusqu'à présent.

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

L'approche directe pour trouver le top 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'affecter à une variable, afin d'utiliser la taille de la liste dans le calcul à 50%.

C'est limitatif, cependant, en ce sens qu'il s'agit d'une représentation "statique" de ce type de filtrage. Comment pourrions-nous la chaîner dans 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 sur son front et que vous enchaînez les choses, personne n'est plus sage. En fait, les opérations de pipeline de flux standard telles que map et filter prennent chacune un flux en entrée et émettent un flux en sortie. Nous pouvons donc écrire une fonction comme celle-ci:

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 des opérations de flux ordinaires:

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

Cela fonctionne, mais est un peu moche car il se lit "à l'envers" et en arrière. Le flux source est widgetList qui est diffusé et filtré via un prédicat ordinaire. Maintenant, en reculant, le filtre 50% supérieur est appliqué, puis le filtre trois plus court est appliqué, et finalement 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 placer ces nouveaux filtres dans une structure de données que nous pouvons manipuler, par exemple, pour exécuter toutes les permutations, comme dans la question d'origine.

Un élément clé à ce stade est que ces nouveaux types de filtres ne sont en réalité que des fonctions, et nous avons des types d’interfaces fonctionnelles en Java qui nous permettent de représenter des fonctions en tant qu’objets, de les manipuler, de les composer, etc. UnaryOperator est un type d'interface fonctionnelle qui prend un argument quelconque et renvoie une valeur du même type. 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 sont trop pour moi. (Mais Aleksey Shipilev m'a montré une fois un 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 que le fait que notre nouveau type d'interface fonctionnelle soit lié à UnaryOperator , donc notre définition peut être simplement:

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

Maintenant, nous pouvons créer une liste de critères comme ceci:

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

(Nous verrons comment utiliser cette liste ci-dessous.) C'est un pas en avant, puisque nous pouvons maintenant manipuler la liste de manière dynamique, mais cela reste un peu limitatif. Tout d'abord, il ne peut pas être combiné avec des prédicats ordinaires. Deuxièmement, il y a beaucoup de valeurs codées en dur, comme les trois plus courtes: qu'en est-il 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 Lambda.

Cela crée un critère qui sélectionne les N premiers widgets, à partir d’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 pourcentage supérieur de widgets, à partir d’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);
}

Nous avons maintenant un moyen très flexible de créer des critères et de les placer dans une liste, où ils peuvent être mis en sous-ensembles ou permutés ou peu importe:

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 identité c -> c est claire, mais le second argument est un peu délicat. Etant donné un flux s nous appliquons d'abord le critère c1, puis le critère c2, et ceci 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 celui-ci:

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 les critères de manière dynamique. Une fois que les objets Criterion se trouvent dans une structure de données, ils peuvent être sélectionnés, sous-évalués, permutés ou autres, et peuvent tous être combinés dans 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 été inventé quelque part déjà, mais c'est nouveau pour Java, car avant lambda, il était impossible d'écrire du code Java utilisant ces techniques.

Mise à jour 2014-04-07

J'ai nettoyé et affiché le code d'échantillon complet dans un esprit.





java-8