Kaherecode

Collections-nous, élégamment avec stream !! (Part 2)

Nas Caso Camara
@NasIsGod 20 févr. 2021
171

Bonjour cher(e)s ami(e)s développeurs et développeuses. J'espère que vous, ainsi que vos familles, vous portiez bien en ces temps de crise. Aujourd'hui, nous allons faire suite au précédent article sur l'API Stream

Comme dans le précédent, le but de cet article est le même : s'éloigner du mal (j'exagère). Je veux parler évidemment, de la boucle For. Plus sérieusement, le but est de vous présenter une manière plus élégante, lisible et efficiente de manipuler les collections.

Si, des termes comme Stream, opérateurs et collections ne vous parlent pas, ce n'est pas grave. Je vous invite à lire la première partie de la série en cliquant sur le lien suivant partie 1.

Nous allons, dans cette suite, choisir quelques opérateurs, et effectuer une comparaison de solutions écrites avec l'api Stream et la boucle For.

Sans plus tarder, entrons dans le vif du sujet. 

Alors, comment va notre animalerie ?

Vous vous souvenez de notre animalerie ? Celle qu'on avait utilisé pour illustrer nos exemples. Nous l'avions représentée sous forme de classe de la manière suivante. 

package business;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class Animal {
    public String nom;
    public int force;
}

Et elle contenait les animaux suivants:

package business;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Animal> animals = List.of(
                Animal.builder()
                        .nom("Elephant")
                        .force(90)
                        .build(),

                Animal.builder()
                        .nom("Lion")
                        .force(100)
                        .build(),
                Animal.builder()
                        .nom("Chien")
                        .force(50)
                        .build(),
                Animal.builder()
                        .nom("Moustique")
                        .force(0).
                        build(),

                Animal.builder()
                        .nom("Fourmie")
                        .force(5)
                        .build()
        );
    }
}

Sommes-nous frappés par cette épidémie?

Depuis le dernier article, le constat est terrible. Une épidémie a frappé les animaleries et fermes de notre région. Les autorités souhaitent recenser toutes les structures touchées. Pour signifier si notre animalerie est touchée, nous allons utiliser un opérateur appelé anyMatch

Avant de commencer, ajoutons une nouvelle propriété à notre classe Animal qui indique si un animal est tombé malade récemment et mettons à jour les données de notre animalerie. Voici à quoi ressemble notre animalerie mise à jour.

package business;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class Animal {
    public String nom;
    public int force;
    public boolean estTombeMalade;
}
package business;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Animal> animals = List.of(
                Animal.builder()
                        .nom("Elephant")
                        .force(90)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Lion")
                        .force(100)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Chien")
                        .force(50)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Moustique")
                        .force(0)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Fourmie")
                        .force(5)
                        .estTombeMalade(true)
                        .build()
                ,
                Animal.builder()
                        .nom("Z&#xE8;bre")
                        .force(30)
                        .estTombeMalade(true)
                        .build()
        );
    }
}

Alors comment l'opérateur anyMatch peut nous aider à avoir cette information ? Cet opérateur retourne un boolean égal à true si au moins un élément du stream respecte le prédicat donné. Donc, logiquement, lorsque nous allons utiliser cet opérateur sur notre collection d'animaux, nous serons en mesure de savoir si notre animalerie a été touchée. Voyons à quoi peu ressembler la solution.

package business;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Animal> animals = List.of(
                Animal.builder()
                        .nom("Elephant")
                        .force(90)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Lion")
                        .force(100)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Chien")
                        .force(50)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Moustique")
                        .force(0)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Fourmie")
                        .force(5)
                        .estTombeMalade(true)
                        .build()
                ,
                Animal.builder()
                        .nom("Z&#xE8;bre")
                        .force(30)
                        .estTombeMalade(true)
                        .build()
        );

        boolean animalerieEstTouche = false;
        for (Animal animal : animals) {
            if (animal.estTombeMalade) {
                animalerieEstTouche = true;
                break;
            }
        }
        System.out.println("Animalerie est-elle touch&#xE9;e : " + animalerieEstTouche);
    }

}

consoleLog

La solution proposée utilise la boucle for pour itérer sur notre collection d'animaux. Elle est fonctionnelle cette solution, mais est-ce qu'elle est simple ? Je ne pense pas. D'abord, le parcours de la boucle. Ensuite, on teste si l'animal courant est tombé malade. Si c'est le cas, alors on conclut que l'animalerie est touchée puis on sort de la boucle avec le break.

Ce code n'est pas vraiment lisible! De plus, supposons qu'on n'avait pas utilisé le break dans notre condition et que, notre animalerie contenait plus d'un million d'animaux. Vous voyez o ù  je veux en venir ? Le gaspillage de ressources qui pourrait se produire. L'itération possible sur plus de 900 milles éléments inutilement? 

Voyons ce que ça donne avec le anyMatch.

package business;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Animal> animals = List.of(
                Animal.builder()
                        .nom("Elephant")
                        .force(90)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Lion")
                        .force(100)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Chien")
                        .force(50)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Moustique")
                        .force(0)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Fourmie")
                        .force(5)
                        .estTombeMalade(true)
                        .build()
                ,
                Animal.builder()
                        .nom("Z&#xE8;bre")
                        .force(30)
                        .estTombeMalade(true)
                        .build()
        );

        boolean animalerieEstTouche = animals.stream().anyMatch(Animal::isEstTombeMalade);
        System.out.println("Animalerie est-elle touch&#xE9;e : " + animalerieEstTouche);
    }

}

consoleLog

Il y a-t-il besoin d'arguments pour justifier la simplicité, la lisibilité de cette solution? Et, il faut noter que tous les éléments du stream ne sont pas évalués systématiquement. Dès lors qu'un élément matche le prédicat, les autres éléments ne sont pas évalués. 

Dans la même idée, il y a l'opérateur allMatch. Cet opérateur retourne vrai lorsque tous les éléments du stream matchent le prédicat donné. Comme le anyMatch, dès lors qu'un élément ne match pas le prédicat, les autres éléments ne sont pas évalués.

Quel animal est le plus touché de notre animalerie ?

Cette épidémie n'a donc pas épargné notre animalerie. Pour réagir au plus vite, nous allons apporter de l'aide à l'animal le plus frappé par la maladie. Pour cela, récupérons l'animal le plus faible de l'animalerie. Pour ce faire, nous utiliserons encore une fois un opérateur, en l'occurrence Min.  

Min est un opérateur qui prend en paramètre,  un Comparator et retourne un Optional.

Comparator est une interface fonctionnelle, nouveauté depuis JAVA 8. Cette interface possède une multitude de fonctions qui prennent, pour la plupart, en paramètre, une autre fonction. Le but étant d'appliquer cette fonction à des objets. Naturellement, la fonction que nous allons passer comme argument est la fonction déterminant le critère de comparaison. Pour ceux ayant déjà fait de l'OCaml, cette notion vous est tout à fait familière. 

Quant à Optional, le type retourné par l'opérateur Min, vous pouvez le voir comme étant une boite dans laquelle peut se trouver ou non un objet. La question que vous vous posez certainement est la suivante : Pourquoi l'opérateur Minretourne un  Optional ? S'il n'y a pas de minimum, il pourrait tout simplement retourner Nullnon? La réponse est simple, le but est de ne pas retourner Null. Comme ça, on évite de se prendre la très célèbre NullPointerException. Optional étant une boite, dès lors que nous la recevons, nous regardons si elle contient une valeur. Si elle en contient, nous la récupérons.

Les notions de Comparator et Optionalmériteraient, chacune, un tutoriel dédié. Et je vous fais la promesse d'en faire là-dessus très rapidement. 

Revenons à notre besoin initial qui était de retrouver l'animal le plus touché par cette maladie et proposons une solution.

package business;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Animal> animals = List.of(
                Animal.builder()
                        .nom("Elephant")
                        .force(90)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Lion")
                        .force(100)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Chien")
                        .force(50)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Moustique")
                        .force(3)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Fourmie")
                        .force(5)
                        .estTombeMalade(true)
                        .build()
                ,
                Animal.builder()
                        .nom("Z&#xE8;bre")
                        .force(30)
                        .estTombeMalade(true)
                        .build()
        );

        Animal animalImpacte = null;
        int forceMin = animals.get(0).force;

    for (int i = 1; i < animals.size(); i++) {
        if(forceMin > animals.get(i).getForce()) {
            forceMin = animals.get(i).force;
            animalImpacte = animals.get(i);
        }
    }
        System.out.println("Animal le plus impact&#xE9;: " + animalImpacte.nom + " Force : " + animalImpacte.getForce());
    }

}

console

Nous avons bien le résultat escompté. Le moustique est l'animal le plus touché par cette épidémie avec sa force seulement égale à 3. Examinons de plus de près ce code. Je pense que vous allez sensiblement être d'accord avec moi qu'il est complexe. D'abord cette boucle qui, pour éviter une itération inutile, commence par l'indice 1. Pour une personne n'ayant pas écrit ce code, l'une des premières questions qu'il se poserait s'il le voyait serait sans doute  " Mais Dieu pourquoi commence-t-il par 1 ???". Question impliquant forcément un temps de réponse considérable ou non selon le niveau d'expérience de la personne.

De plus, pendant l'itération, on veille à stocker deux variables. L'une contenant la force minimale au moment de l'itération courante et l'autre, contenant l'animal ayant cette force. Pour trouver la force minimale, on a donc besoin de deux variables. C'est vraiment too much pour une simple opération à la base. 

Maintenant, laissons la magie de l'API Stream opérer et voyons ce que cela donne.

package business;

import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Animal> animals = List.of(
                Animal.builder()
                        .nom("Elephant")
                        .force(90)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Lion")
                        .force(100)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Chien")
                        .force(50)
                        .estTombeMalade(false)
                        .build(),
                Animal.builder()
                        .nom("Moustique")
                        .force(3)
                        .estTombeMalade(false)
                        .build(),

                Animal.builder()
                        .nom("Fourmie")
                        .force(5)
                        .estTombeMalade(true)
                        .build()
                ,
                Animal.builder()
                        .nom("Z&#xE8;bre")
                        .force(30)
                        .estTombeMalade(true)
                        .build()
        );

        Animal animalImpacte = animals.stream().min(Comparator.comparingInt(Animal::getForce)).get();
        System.out.println("Animal le plus impact&#xE9;: " + animalImpacte.nom + " Force : " + animalImpacte.getForce());
    }

}

console

Le résultat est le même. Le moustique est toujours l'animal le plus impacté de notre animalerie. Mais que dire du code ? La solution tient en une ligne

Comme d'habitude, nous avons ouvert un flux en appelant stream() ensuite l'opérateur terminal Min. À l'opérateur, nous avons passé la fonction Comparator.comparingInt. Cette fonction, comme son nom l'indique, permet de comparer des nombres entiers. Ces nombres, elle les extrait à travers une fonction qu'elle prend en paramètre aussi. Ici, il s'agit du getForce de la classe Animal

Nous avons ensuite l' Optional  qui nous est retourné. Vous vous rappelez, nous avons dit que l' Optional  était comme une boite. Dans cet exemple, cette boite contient l'animal le plus impacté. Nous utilisons ensuite la fonction Get fournie par la classe Optional  pour récupérer le contenu de cette boite. 

Nous finissons cette seconde partie sur l'API stream avec cet exemple. Nous avons pu voir les opérateurs anyMatch, Min, et AllMatch et Max par ricochet. Dans la troisième et dernière partie, nous allons voir la manière de récupérer nos objets. À bientôt les amis !!!!!!!!!!!


Partage ce tutoriel


Merci à

Nas Caso Camara

Nas Caso Camara

@NasIsGod

Développeur FullStack et grand fan de musique, travaillant essentiellement sur les technos JAVA et Angular 2+.

Continue de lire

Discussion

Tu dois être connecté pour participer à la discussion. Me connecter.