Archives Wild Code School
Java Avancé

Stream 01 : Filter

Quête 14 sur 15

Depuis Java 8, il existe un moyen très pratique de travailler avec les collections : les Stream. Cette interface te simplifiera la vie quand tu voudras filtrer, trier ou encore effectuer des opérations sur des listes d'objets.

Cependant, les Stream vont te demander des notions sur des concepts complexes, introduits avec Java 8 :

  • les interfaces fonctionnelles
  • les lambdas

Pour t'aider à comprendre comment utiliser les Stream dans le cas du filtrage, tu vas voir dans cette quête comment tout cela fonctionne, étape par étape.

Objectifs

  • Trier une liste d'objets avec filter
  • Utiliser l'interface fonctionnelle Predicate
  • Comprendre et utiliser les lambdas

Filter

Avant de voir comment utiliser les Stream pour filtrer une liste, regarde d'abord comment procéder avec tes connaissances actuelles.

Partons d'une liste d'entiers, contenant des nombres de 0 à 6.

java
List<Integer> list = new ArrayList<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6));

Tu veux filtrer la liste pour ne garder que les nombres pairs. Tu vas devoir itérer chaque élément de la liste et vérifier s'il est pair : si c'est le cas, il faudra le stocker dans une autre liste.

01. Itération

java
List<Integer> listFiltered = new ArrayList<>();

for (Integer item : list) {
    if (item % 2 == 0) {
        listFiltered.add(item);
    }
}

Si tu affiches le contenu de la nouvelle liste filtrée, tu obtiendras le résultat suivant :

java
System.out.println(listFiltered.toString());

[0, 2, 4, 6]

02. Créer une méthode

Maintenant, tu peux isoler le code te permettant de filtrer dans une méthode, de la façon suivante :

java
private static List<Integer> filter(List<Integer> list) {
    List<Integer> listFiltered = new ArrayList<>();
    for (Integer item : list) {
        if (item % 2 == 0) {
            listFiltered.add(item);
        }
    }
    return listFiltered;
}

Tu pourras désormais filtrer n'importe quelle liste d'entiers de la façon suivante :

java
List<Integer> listFiltered = filter(list);

Predicate

Dans le cas que tu as vu précédemment, la condition de filtrage est contenue dans le parcours de la boucle et donc dans la méthode filter. C'est la ligne :

java
if (item % 2 == 0)

Imaginons que tu veuilles améliorer ta méthode afin de passer en paramètre la condition de filtrage : tu pourrais ainsi utiliser la même méthode filter afin de filtrer les nombres pairs, les multiples de trois, les nombres supérieurs à 10, etc.

Il va te falloir appliquer une fonction de test sur chacun des nombres. En Java, on résout cette problématique en créant une interface.

01. Créer son interface de filtrage

Ton interface va décider si l'élément sera gardé, ou non, dans la liste. Pour l'exemple, elle va s'appeler Keep. Cette interface ne va avoir qu'une signature de méthode : test. Selon l'entier en argument, elle va répondre s'il doit être gardé, ou non (il s'agit donc d'un booléen) :

java
private interface Keep {

    boolean test(Integer item);
}

C'est une interface, on ne donne donc pas de corps à la méthode. C'est logique, la façon de garder l'élément dépendra de la liste : comme dit plus haut, on pourrait vouloir garder les nombres pairs dans un cas, ou les multiples de trois dans un autre cas.

Il va te falloir modifier ta méthode filter afin d'accepter en argument une instance de l'interface Keep :

java
private static List<Integer> filter(List<Integer> list, Keep keep) {
    List<Integer> listFiltered = new ArrayList<>();
    for (Integer item : list) {
        if (keep.test(item)) {
            listFiltered.add(item);
        }
    }
    return listFiltered;
}

Mais si tu te rappelles les cours, il n'est pourtant pas possible de créer une instance d'une interface !

Java te permet de créer une classe anonyme, qui implémente une interface et en crée une instance en même temps :

java
new Keep() {

    @Override
    public boolean test(Integer item) {
        return item % 2 == 0;
    }
};

Tu peux alors passer la classe anonyme en paramètre de la méthode filter :

java
List<Integer> listFiltered = filter(list, new Keep() {

    @Override
    public boolean test(Integer item) {
        return item % 2 == 0;
    }
});

02. Interface fonctionnelle

Donc, maintenant que tu as vu comment créer une interface et passer un classe anonyme en paramètre, sache que depuis Java 8, il est possible d'utiliser un certain nombre d'interfaces fonctionnelles déjà existantes.

Celle qui va nous intéresser ici est Predicate :

java
@FunctionalInterface
public interface Predicate<T> {

    boolean test(T var1);

    // few other default methods here
}

Ici T représente une classe générique : si tu n'as pas encore vu ce concept, sache qu'elle représente n'importe quelle classe objet. Pour plus d'informations sur le sujet, consulte l'article correspondant en ressource.

Un Predicate est à utiliser dans le cas d'un test à faire : en cas de réussite, la méthode test retournera true, sinon false.

Comme tu remarques, elle est presque identique à l'interface Keep créée précédemment. Tu peux donc créer une classe anonyme à sa place, de la façon suivante :

java
List<Integer> listFiltered = filter(list, new Predicate<Integer>() {
    @Override
    public boolean test(Integer item) {
        return item % 2 == 0;
    }
});

Cependant, comme ta méthode filter s'attend à recevoir un objet de type Keep, il va aussi falloir modifier l'argument par un Predicate :

java
private static List<Integer> filter(List<Integer> list, Predicate<Integer> keep) {
    List<Integer> listFiltered = new ArrayList<>();
    for (Integer item : list) {
        if (keep.test(item)) {
            listFiltered.add(item);
        }
    }
    return listFiltered;
}

↗
The Basics of Java Generics
baeldung.com
↗
Java Predicate Example - Predicate Filter
howtodoinjava.com
↗
Java Anonymous Class
journaldev.com

Lambdas

Depuis Java 8, du fait de l'utilisation des interfaces fonctionnelles, il est devenu important de trouver une approche moins verbeuse de leurs appels en classes anonymes.

C'est pourquoi les lambdas (ou fonctions fléchées) ont été introduites : elles permettent de fortement réduire une classes anonymes créée à partir d'une interface fonctionnelle (uniquement dans le cas où elle ne contient qu'une seule méthode à implémenter).

Prenons l'exemple de notre Predicate, lors de l'appel de la méthode filter :

java
filter(list, new Predicate<Integer>() {
    @Override
    public boolean test(Integer item) {
        return item % 2 == 0;
    }
});

La méthode filter sait déjà qu'elle s'attend à un Predicate en argument : il est donc inutile de le lui préciser. De plus, l'interface Predicate ne contient qu'une seule méthode à implémenter : pas besoin de le préciser non plus.

Une Lambda se présente sous la forme suivante :

java
(arguments, ...) -> {
    // body
}

Dans le cas de l'appel de Predicate par filter, il devient donc possible de l'écrire de la façon suivante :

java
(item) -> {
    return item % 2 == 0;
}

Mais ce n'est pas tout : il est possible de simplifier encore plus ! En effet, si une méthode ne possède qu'un seul argument, les lambdas permettent d'enlever les parenthèses. De plus, si le corps de la méthode ne contient que le retour d'une expression, il t'est possible d'enlever les accolades et le mot-clé return (le retour sera fait de manière implicite) :

java
item -> item % 2 == 0

Pour plus d'informations sur l'utilisation des lambdas, consulte l'article correspondant en ressource.

Il t'est alors possible de simplifier l'appel de filter afin d'arriver au code suivant :

java
List<Integer> listFiltered = filter(list, item -> item % 2 == 0);

↗
How to start working with Lambda Expressions in Java
medium.freecodecamp.org

Stream

Les étapes précédentes t'ont montré comment on faisait avant Java 8 pour filtrer une liste, ainsi que l'utilisation d'une interface fonctionnelle et des lambdas.

Maintenant, tu vas découvrir la puissance des Stream et de l'utilisation de filter !

Un avantage des Stream, c'est qu'ils distribuent la charge de travail sur plusieurs threads. Ton processeur contient plusieurs cœurs, qui eux-mêmes ont plusieurs threads : tu peux voir ça comme une autoroute à plusieurs voies. Avec une boucle classique, toutes les voitures sont sur la même voie : ce n'est pas très efficace, si une voiture va lentement, tout le monde est ralenti ! En utilisant un Stream, les voitures conduisent sur plusieurs voies : il y a donc moins de chances que toutes les voies soient encombrées !

L'utilisation d'un Stream comporte trois étapes :

  • Source : on crée un flux (stream), par exemple à partir d'une liste d'éléments, d'un tableau...

  • Intermediate : on enchaîne une ou plusieurs opérations, par exemple filtrer, trier...

  • Terminal : on regroupe les résultats, par exemple on les collecte sous forme de liste, on en fait une somme...

Dans le cas qui nous intéresse pour cette quête, nous allons voir un exemple avec l'étape intermédiaire filter.

01. Source : List

La création d'un stream à partir d'une List est très simple ! Il suffit d'appeler la méthode stream sur la liste :

java
list.stream()

Ici list est un nom de variable, il faudra le modifier par ta liste !

02. Intermediate : Filter

Tu vas donc pouvoir appliquer ton filtrage avec un appel de la méthode filter : comme un filtre attend une réponse "garder ou non" pour chaque élément, elle attend en argument un Predicate contenant le test à effectuer.

java
list.stream()
        .filter(item -> item % 2 == 0)

03. Terminal : Collect

Le simple fait de filtrer n'est pas suffisant : en utilisant un Stream, la liste d'origine ne sera pas modifiée. Il faut collecter le résultat.

Cela se fait avec l'utilisation de la méthode collect, en lui précisant qu'on veut un résultat sous forme de List. Il faut alors stocker le retour dans une nouvelle liste :

java
List<Integer> listFiltered = list.stream()
        .filter(item -> item % 2 == 0)
        .collect(Collectors.toList());

*Important : * tu trouveras en ressource de cette section le code de toutes les étapes vues dans la quête. Les noms des classes vont de 1 à 6, progressant de la méthode "à l'ancienne" jusqu'à l'utilisation des Stream et lambdas.

↗
Dépôt contenant tous les exemples de la quête
github.com
↗
Java Stream Filter with Lambda Expression
baeldung.com

Challenge

Fichage des héros

Après des années à postuler, tu as enfin été embauché au S.H.I.E.L.D ! Malheureusement, ton premier boulot consiste à aider le président du CE (Comité d'Entreprise) pour des tâches ingrates. Peu importe, c'est pour le bien de la Terre, alors tu t'exécutes !

Le président te fournit une liste de héros, et te donne les tâches suivantes :

  • le CE a négocié un avantage avec Tony Stark pour réaliser des cartes seniors : -30% sur des pantoufles en vibranium. Ta première mission consiste à trouver la liste des héros de 60 ans et plus !
  • le goûter d'anniversaire de Thor sera organisé la semaine prochaine : afin d'éviter les drames, ta seconde mission est de trouver la liste des héros intolérants au gluten !

Pour démarrer, fais un Fork du dépôt suivant puis clone-le en local.

Pense bien à faire un Fork, sinon tu ne pourras rien pousser !

  1. Ouvre les classes Hero et Shield afin d'en étudier leurs contenus respectifs.
  2. Dans Shield.java, utilise un Stream et la méthode filter pour récupérer dans elders la liste des héros de 60 ans et plus
  3. Dans Shield.java, utilise un Stream et la méthode filter pour récupérer dans intolerants la liste des héros intolérants au gluten
  4. Dans les deux cas précédents, utilise un Predicate et des lambdas
  5. Tu ne dois pas modifier la classe Hero !
  6. Compile et exécute le code, afin de vérifier que les deux listes s'affichent bien dans le terminal

Résultat attendu lors de l'exécution de Shield :

shell
$ Elders:
$ Captain America
$ Thor

$ Gluten intolerants:
$ Vision
$ Scarlet Witch
$ Hulk

Critères de validation

  • Le dépôt GitHub contient bien les fichiers Hero.java et Shield.java. Seul Shield.java doit avoir été modifié.
  • La classe Shield contient bien un Stream qui récupère la liste des héros de 60 ans et plus, en utilisant la méthode filter et un Predicate
  • La classe Shield contient bien un Stream qui récupère la liste des héros intolérants au gluten, en utilisant la méthode filter et un Predicate
  • La classe Shield se compile sans erreur et affiche dans le terminal le même résultat que celui présenté précédemment