Chapitre 2 : Utilisation des collections
Nous utilisons souvent diverses collections, nombres, chaînes et objets. Ils sont partout, et même si le code qui gère la collection peut être légèrement optimisé, cela rendra le code beaucoup plus clair. Dans ce chapitre, nous explorons comment utiliser les expressions lambda pour manipuler des collections. Nous l'utilisons pour parcourir les collections, convertir des collections en nouvelles collections, supprimer des éléments des collections et fusionner des collections.
Parcourez la liste
Le parcours d'une liste est l'opération d'ensemble la plus élémentaire, et ses opérations ont subi quelques changements au fil des ans. Nous utilisons un petit exemple de traversée de noms, en l'introduisant de la version la plus ancienne à la version la plus élégante aujourd'hui.
Nous pouvons facilement créer une liste de noms immuable avec le code suivant :
Copiez le code comme suit :
liste finale<String> amis =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
System.out.println(friends.get(i));
}
Ce qui suit est la méthode la plus courante pour parcourir une liste et l’imprimer, bien qu’elle soit également la plus générale :
Copiez le code comme suit :
for(int i = 0; i < amis.size(); i++) {
System.out.println(friends.get(i));
}
J’appelle cette façon d’écrire masochiste – c’est verbeux et sujet aux erreurs. Nous devons nous arrêter et réfléchir : « Est-ce que c'est i< ou i<= ? » Cela n'a de sens que lorsque nous devons opérer sur un élément spécifique, mais même dans ce cas, nous pouvons toujours utiliser des expressions fonctionnelles qui adhèrent au principe de style d’immuabilité, dont nous parlerons sous peu.
Java fournit également une structure relativement avancée.
Copiez le code comme suit :
collections/fpij/Itération.java
pour(Nom de la chaîne : amis) {
System.out.println(nom);
}
Sous le capot, l'itération de cette manière est implémentée à l'aide de l'interface Iterator, appelant ses méthodes hasNext et next. Les deux méthodes sont des itérateurs externes et combinent la manière de le faire avec ce que vous voulez faire. Nous contrôlons explicitement l'itération, en lui indiquant où commencer et où finir ; la deuxième version le fait en coulisse via la méthode Iterator. Dans le cadre d'une opération explicite, vous pouvez également utiliser les instructions break et continue pour contrôler l'itération. La deuxième version comporte quelques éléments manquants par rapport à la première. Cette approche est meilleure que la première si l'on n'a pas l'intention de modifier un élément de la collection. Cependant, ces deux méthodes sont impératives et devraient être abandonnées dans le Java actuel. Il y a plusieurs raisons de passer à un style fonctionnel :
1. La boucle for elle-même est en série et difficile à paralléliser.
2. Une telle boucle n’est pas polymorphe ; ce que vous obtenez est ce que vous demandez. Nous passons la collection directement à la boucle for, plutôt que d'appeler une méthode (qui prend en charge le polymorphisme) sur la collection pour effectuer une opération spécifique.
3. Du point de vue de la conception, le code écrit de cette manière viole le principe « Dites, ne demandez pas ». Nous demandons qu'une itération soit effectuée plutôt que de laisser l'itération à la bibliothèque sous-jacente.
Il est temps de passer de l'ancienne programmation impérative à la programmation fonctionnelle plus élégante des itérateurs internes. Après avoir utilisé des itérateurs internes, nous laissons de nombreuses opérations spécifiques à la bibliothèque de méthodes sous-jacente pour exécution, afin que vous puissiez vous concentrer davantage sur les exigences métier spécifiques. La fonction sous-jacente sera responsable de l'itération. Nous utilisons d’abord un itérateur interne pour énumérer la liste des noms.
L'interface Iterable a été améliorée dans JDK8. Elle porte un nom spécial appelé forEach, qui reçoit un paramètre de type Comsumer. Comme son nom l'indique, une instance Consumer consomme l'objet qui lui est transmis via sa méthode accept. Nous utilisons la syntaxe familière des classes internes anonymes pour utiliser la méthode forEach :
Copiez le code comme suit :
friends.forEach(new Consumer<String>() { public void accept(final String name) {
System.out.println(nom);
});
Nous avons appelé la méthode forEach sur la collection friends, en lui transmettant une implémentation anonyme de Consumer. Cette méthode forEach appelle la méthode accept du consommateur transmis pour chaque élément de la collection, lui permettant de traiter cet élément. Dans cet exemple, nous imprimons simplement sa valeur, qui est le nom. Jetons un coup d'œil au résultat de cette version, qui est le même que les deux précédentes :
Copiez le code comme suit :
Brian
Nate
Neal
Raju
Sarah
Scott
Nous n'avons changé qu'une chose : nous avons abandonné la boucle for obsolète et utilisé un nouvel itérateur interne. L'avantage est que nous n'avons pas besoin de spécifier comment itérer la collection et pouvons nous concentrer davantage sur la façon de traiter chaque élément. L'inconvénient est que le code semble plus verbeux - ce qui tue presque la joie du nouveau style de codage. Heureusement, cela est facile à changer, et c'est là que la puissance des expressions lambda et des nouveaux compilateurs entre en jeu. Apportons une modification supplémentaire et remplaçons la classe interne anonyme par une expression lambda.
Copiez le code comme suit :
amis.forEach((nom de la chaîne finale) -> System.out.println(nom));
C'est beaucoup mieux ainsi. Il y a moins de code, mais regardons d'abord ce que cela signifie. La méthode forEach est une fonction d'ordre supérieur qui reçoit une expression lambda ou un bloc de code pour opérer sur les éléments de la liste. A chaque appel, les éléments de la collection seront liés à la variable name. La bibliothèque sous-jacente héberge l'activité d'invocation d'expression lambda. Il peut décider de retarder l'exécution des expressions et, le cas échéant, effectuer des calculs parallèles. Le résultat de cette version est également le même que la précédente.
Copiez le code comme suit :
Brian
Nate
Neal
Raju
Sarah
Scott
La version de l'itérateur interne est plus concise. De plus, en l'utilisant, nous pouvons nous concentrer davantage sur le traitement de chaque élément au lieu de le parcourir - c'est déclaratif.
Cependant, cette version présente des défauts. Une fois que la méthode forEach commence à s'exécuter, contrairement aux deux autres versions, nous ne pouvons pas sortir de cette itération. (Bien sûr, il existe d'autres façons de procéder). Par conséquent, cette façon d’écrire est plus couramment utilisée lorsque chaque élément de la collection doit être traité. Plus tard, nous présenterons d'autres fonctions qui nous permettent de contrôler le processus de boucle.
La syntaxe standard des expressions lambda consiste à placer les paramètres entre (), à fournir des informations de type et à utiliser des virgules pour séparer les paramètres. Afin de nous libérer, le compilateur Java peut également effectuer automatiquement une déduction de type. Bien sûr, il est plus pratique de ne pas écrire de caractères. Il y a moins de travail et le monde est plus calme. Voici la version précédente après suppression du paramètre type :
Copiez le code comme suit :
amis.forEach((nom) -> System.out.println(nom));
Dans cet exemple, le compilateur Java sait que le type de nom est String grâce à l'analyse du contexte. Il regarde la signature de la méthode appelée forEach puis analyse l'interface fonctionnelle dans les paramètres. Ensuite, il analysera la méthode abstraite dans cette interface et vérifiera le nombre et le type de paramètres. Même si cette expression lambda reçoit plusieurs paramètres, nous pouvons toujours effectuer une déduction de type, mais dans ce cas, tous les paramètres ne peuvent pas avoir de types de paramètres dans les expressions lambda, les types de paramètres doivent être écrits, ou s'ils sont écrits, ils doivent être écrits ; au complet.
Le compilateur Java traite spécialement les expressions lambda avec un seul paramètre : si vous souhaitez effectuer une inférence de type, les parenthèses autour du paramètre peuvent être omises.
Copiez le code comme suit :
amis.forEach(nom -> System.out.println(nom));
Il y a une petite mise en garde ici : les paramètres utilisés pour l'inférence de type ne sont pas du type final. Dans l’exemple précédent de déclaration explicite d’un type, nous avons également marqué le paramètre comme final. Cela vous empêche de modifier la valeur du paramètre dans l'expression lambda. D'une manière générale, c'est une mauvaise habitude de modifier la valeur d'un paramètre, ce qui peut facilement provoquer un BUG, c'est donc une bonne habitude de le marquer comme final. Malheureusement, si nous voulons utiliser l'inférence de type, nous devons suivre les règles nous-mêmes et ne pas modifier les paramètres, car le compilateur ne nous protège plus.
Il a fallu beaucoup d'efforts pour en arriver là, mais maintenant la quantité de code est effectivement un peu plus petite. Mais ce n’est pas encore le plus simple. Essayons cette dernière version minimaliste.
Copiez le code comme suit :
amis.forEach(System.out::println);
Dans le code ci-dessus, nous utilisons une référence de méthode. Nous pouvons directement remplacer l’intégralité du code par le nom de la méthode. Nous explorerons cela en profondeur dans la section suivante, mais pour l’instant rappelons une célèbre citation d’Antoine de Saint-Exupéry : la perfection n’est pas ce qui peut être ajouté, mais ce qui ne peut plus être retiré.
Les expressions Lambda nous permettent de parcourir les collections de manière concise et claire. Dans la section suivante, nous expliquerons comment cela nous permet d'écrire un code aussi concis lors de l'exécution d'opérations de suppression et de conversions de collections.