Implémenter l'interface Comparateur
L'interface Comparator est visible partout dans la bibliothèque JDK, de la recherche au tri en passant par les opérations inverses, etc. Dans Java 8, cela devient une interface fonctionnelle. L'avantage est que nous pouvons utiliser la syntaxe streaming pour implémenter le comparateur.
Implémentons Comparator de plusieurs manières différentes pour voir la valeur de la nouvelle syntaxe. Vos doigts vous remercieront. Ne pas avoir à implémenter de classes internes anonymes vous évite beaucoup de frappes.
Tri à l'aide du comparateur
L'exemple suivant utilisera différentes méthodes de comparaison pour trier un groupe de personnes. Créons d'abord un Person JavaBean.
Copiez le code comme suit :
classe publique Personne {
Nom de chaîne final privé ;
âge int final privé ;
personne publique (string final theName, final int theAge) {
nom = leNom ;
âge = l'âge ;
}
public String getName() { nom de retour }
public int getAge() { âge de retour ;
public int ageDifference (personne finale autre) {
âge de retour - autre.âge ;
}
chaîne publique versChaîne() {
return String.format("%s - %d", nom, âge);
}
}
Nous pouvons implémenter l'interface Comparator via la classe Person, mais de cette façon, nous ne pouvons utiliser qu'une seule méthode de comparaison. Nous voulons pouvoir comparer différents attributs, tels que le nom, l'âge ou une combinaison de ceux-ci. Afin d'effectuer une comparaison de manière flexible, nous pouvons utiliser Comparator pour générer du code pertinent lorsque nous devons comparer.
Créons d'abord une liste de personnes, chacune avec un nom et un âge différents.
Copiez le code comme suit :
liste finale<Person> people = Arrays.asList(
nouvelle Personne("Jean", 20),
nouvelle personne("Sara", 21),
nouvelle personne("Jane", 21),
nouvelle personne("Greg", 35));
Nous pouvons trier les personnes par ordre croissant ou décroissant selon leur nom ou leur âge. La méthode générale consiste à utiliser des classes internes anonymes pour implémenter l'interface Comparator. S’il est écrit de cette façon, seul le code le plus pertinent a du sens, et le reste n’est qu’une formalité. L'utilisation d'expressions lambda peut se concentrer sur l'essence de la comparaison.
Trions-les d’abord par âge, du plus jeune au plus âgé.
Maintenant que nous avons un objet List, nous pouvons utiliser sa méthode sort() pour trier. Cependant, cette méthode a aussi ses problèmes. Il s'agit d'une méthode vide, ce qui signifie que lorsque nous appellerons cette méthode, la liste changera. Pour conserver la liste d'origine, il faut d'abord en faire une copie puis appeler la méthode sort(). C'était tout simplement trop d'effort. À ce stade, nous devons nous tourner vers la classe Stream pour obtenir de l'aide.
Nous pouvons obtenir un objet Stream de la liste, puis appeler sa méthode sorted(). Il renvoie une collection triée plutôt que de modifier la collection originale. En utilisant cette méthode, vous pouvez facilement configurer les paramètres de Comparator.
Copiez le code comme suit :
Liste<Personne> ascendingAge =
personnes.stream()
.sorted((person1, personne2) -> personne1.ageDifference(person2))
.collect(toList());
printPeople("Trié par ordre croissant par âge : ", ascendingAge);
Nous convertissons d’abord la liste en un objet Stream via la méthode stream(). Appelez ensuite sa méthode sorted(). Cette méthode accepte un paramètre Comparator. Puisque Comparator est une interface fonctionnelle, nous pouvons transmettre une expression lambda. Enfin, nous appelons la méthode collect et lui faisons stocker les résultats dans une liste. La méthode collect est un réducteur qui peut générer les objets pendant le processus d'itération dans un format ou un type spécifique. La méthode toList() est une méthode statique de la classe Collectors.
La méthode abstraite compareTo() de Comparator reçoit deux paramètres, qui sont les objets à comparer, et renvoie un résultat de type int. Pour être compatible avec cela, notre expression lambda reçoit également deux paramètres, deux objets Person, dont les types sont automatiquement déduits par le compilateur. Nous renvoyons un type int indiquant si les objets comparés sont égaux.
Parce que nous voulons trier par âge, nous comparerons les âges des deux objets puis renverrons le résultat de la comparaison. S'ils sont de la même taille, renvoyez 0. Sinon, un nombre négatif est renvoyé si la première personne est plus jeune et un nombre positif est renvoyé si la première personne est plus âgée.
La méthode sorted() parcourra chaque élément de la collection cible et appellera le Comparator spécifié pour déterminer l'ordre de tri des éléments. La méthode d'exécution de la méthode sorted() est quelque peu similaire à la méthode réduire() mentionnée précédemment. La méthode réduire() réduit progressivement la liste à un résultat. La méthode sorted() trie selon les résultats de la comparaison.
Une fois que nous avons trié, nous voulons imprimer les résultats, nous appelons donc une méthode printPeople() ; implémentons cette méthode.
Copiez le code comme suit :
public static void printPeople(
message de chaîne final, liste finale<Person> personnes) {
System.out.println(message);
people.forEach(System.out::println);
}
Dans cette méthode, nous imprimons d’abord un message, puis parcourons la liste et imprimons chaque élément qu’elle contient.
Appelons la méthode sorted() pour voir comment elle triera les personnes de la liste du plus jeune au plus âgé.
Copiez le code comme suit :
Classés par ordre croissant d'âge :
Jean - 20
Sarah - 21
Jeanne - 21
Grégory - 35
Jetons un autre regard sur la méthode sorted() pour apporter une amélioration.
Copiez le code comme suit :
.sorted((person1, personne2) -> personne1.ageDifference(person2))
Dans l'expression lambda transmise, nous acheminons simplement ces deux paramètres - le premier paramètre est utilisé comme cible d'appel de la méthode ageDifference() et le deuxième paramètre est utilisé comme son paramètre. Mais nous ne pouvons pas l'écrire comme ça, mais utiliser un mode espace bureau, c'est-à-dire utiliser des références de méthode et laisser le compilateur Java effectuer le routage.
Le routage des paramètres utilisé ici est un peu différent de ce que nous avons vu précédemment. Nous avons vu précédemment que les arguments sont passés soit comme cibles d'appel, soit comme paramètres d'appel. Maintenant, nous avons deux paramètres et nous voulons les diviser en deux parties, l'une comme cible de l'appel de méthode et la seconde comme paramètre. Ne vous inquiétez pas, le compilateur Java vous dira : "Je m'en occupe".
Nous pouvons remplacer l'expression lambda dans la méthode sorted() précédente par une méthode ageDifference courte et concise.
Copiez le code comme suit :
personnes.stream()
.sorted(Personne::ageDifference)
Ce code est très concis, grâce aux références de méthodes fournies par le compilateur Java. Le compilateur reçoit deux paramètres d'instance personne et utilise le premier comme cible de la méthode ageDifference() et le second comme paramètre de méthode. Nous laissons le compilateur faire ce travail au lieu d'écrire le code directement. Lors de l'utilisation de cette méthode, nous devons nous assurer que le premier paramètre est la cible appelante de la méthode référencée et que le reste est le paramètre d'entrée de la méthode.
Comparateur de réutilisation
Il est facile de trier les personnes de la liste du plus jeune au plus âgé, et il est également facile de trier du plus vieux au plus jeune. Essayons.
Copiez le code comme suit :
printPeople("Triés par ordre décroissant d'âge : ",
personnes.stream()
.sorted((person1, personne2) -> personne2.ageDifference(person1))
.collect(toList()));
Nous appelons la méthode sorted() et transmettons une expression lambda, qui s'intègre dans l'interface Comparator, tout comme dans l'exemple précédent. La seule différence est l'implémentation de cette expression lambda : nous avons modifié l'ordre des personnes à comparer. Les résultats doivent être classés du plus âgé au plus jeune en fonction de leur âge. Jetons un coup d'oeil.
Copiez le code comme suit :
Classés par ordre décroissant par âge :
Grégory - 35
Sarah - 21
Jeanne - 21
Jean - 20
Il ne faut pas beaucoup d’efforts pour simplement changer la logique de comparaison. Mais nous ne pouvons pas reconstruire cette version en référence de méthode car l'ordre des paramètres n'est pas conforme aux règles de routage des paramètres pour les références de méthode ; le premier paramètre n'est pas utilisé comme cible appelante de la méthode, mais comme paramètre de méthode ; Il existe un moyen de résoudre ce problème qui réduit également la duplication des efforts. Voyons comment procéder.
Nous avons déjà créé deux expressions lambda : l'une consiste à trier par âge de petit à grand, et l'autre consiste à trier de grand à petit. Cela entraînerait une redondance et une duplication du code et violerait le principe DRY. Si nous voulons simplement ajuster l'ordre de tri, JDK fournit une méthode inverse, qui a un modificateur de méthode spécial, par défaut. Nous en discuterons dans la méthode par défaut à la page 77. Ici, nous utilisons d'abord la méthode reverse() pour supprimer la redondance.
Copiez le code comme suit :
Comparateur<Personne> compareAscending =
(personne1, personne2) -> personne1.ageDifference(personne2);
Comparator<Person> compareDescending = compareAscending.reversed();
Nous avons d'abord créé un comparateur, compareAscending, pour trier les personnes par âge, du plus jeune au plus âgé. Afin d'inverser l'ordre de comparaison, au lieu de réécrire ce code, il suffit d'appeler la méthode reversed() du premier Comparator pour obtenir le deuxième objet Comparator. Sous le capot de la méthode reversed(), elle crée un comparateur pour inverser l’ordre des paramètres comparés. Cela montre que reversed est également une méthode d'ordre supérieur - elle crée et renvoie une fonction sans effets secondaires. Nous utilisons ces deux comparateurs dans le code.
Copiez le code comme suit :
printPeople("Triés par ordre croissant par âge : ",
personnes.stream()
.sorted (comparerAscending)
.collect(toList())
);
printPeople("Triés par ordre décroissant d'âge : ",
personnes.stream()
.sorted(comparerDescending)
.collect(toList())
);
Il ressort clairement du code que ces nouvelles fonctionnalités de Java8 ont considérablement réduit la redondance et la complexité du code, mais les avantages sont bien plus que ceux-ci. Des possibilités infinies vous attendent dans le JDK.
Nous pouvons déjà trier par âge, et il est également facile de trier par nom. Trions-les lexicographiquement par nom. De même, il suffit de changer la logique dans l'expression lambda.
Copiez le code comme suit :
printPeople("Trié par ordre croissant par nom : ",
personnes.stream()
.sorted((personne1, personne2) ->
personne1.getName().compareTo(person2.getName()))
.collect(toList()));
Les résultats de sortie seront triés par ordre lexicographique par nom.
Copiez le code comme suit :
Triés par ordre croissant de nom :
Grégory - 35
Jeanne - 21
Jean - 20
Sarah - 21
Jusqu'à présent, nous avons trié soit par âge, soit par nom. Nous pouvons rendre la logique des expressions lambda plus intelligente. Par exemple, nous pouvons trier par âge et par nom en même temps.
Choisissons la personne la plus jeune de la liste. On peut d'abord trier par âge du plus petit au plus grand puis sélectionner le premier dans les résultats. Mais cela ne fonctionne pas réellement. Stream a une méthode min() pour y parvenir. Cette méthode accepte également un Comparator, mais renvoie le plus petit objet de la collection. Utilisons-le.
Copiez le code comme suit :
personnes.stream()
.min (Personne :: différence d'âge)
.ifPresent(le plus jeune -> System.out.println("Le plus jeune : " + le plus jeune));
Lors de l’appel de la méthode min(), nous avons utilisé la référence de la méthode ageDifference. La méthode min() renvoie un objet Optinal car la liste peut être vide et contenir plusieurs personnes les plus jeunes. Ensuite, nous obtenons la personne la plus jeune via la méthode ifPrsend() d'Optinal et imprimons ses informations détaillées. Jetons un coup d'œil au résultat.
Copiez le code comme suit :
Le plus jeune : Jean - 20 ans
Exporter le plus ancien est également très simple. Passez simplement cette référence de méthode à une méthode max().
Copiez le code comme suit :
personnes.stream()
.max(Personne::ageDifference)
.ifPresent(aîné -> System.out.println("Aîné : " + aîné));
Jetons un coup d'œil au nom et à l'âge de l'aîné.
Copiez le code comme suit :
Aîné : Greg - 35 ans
Avec les expressions lambda et les références de méthodes, la mise en œuvre des comparateurs devient plus simple et plus pratique. JDK introduit également de nombreuses méthodes pratiques dans la classe Compararor, nous permettant de comparer plus facilement, comme nous le verrons ci-dessous.
Comparaisons multiples et comparaisons en streaming
Jetons un coup d'œil aux nouvelles méthodes pratiques fournies par l'interface Comparator et utilisons-les pour comparer plusieurs propriétés.
Continuons en utilisant l'exemple de la section précédente. En triant par nom, voici ce que nous avons écrit ci-dessus :
Copiez le code comme suit :
personnes.stream()
.sorted((personne1, personne2) ->
personne1.getName().compareTo(person2.getName()));
Comparée à la méthode d'écriture de classe interne du siècle dernier, cette méthode d'écriture est tout simplement trop simple. Cependant, cela peut être simplifié si nous utilisons certaines fonctions de la classe Comparator. L'utilisation de ces fonctions peut nous permettre d'exprimer notre objectif plus facilement. Par exemple, si on veut trier par nom, on peut écrire :
Copiez le code comme suit :
final Function<Person, String> byName = personne -> personne.getName();
personnes.stream()
.sorted(comparaison(byName));
Dans ce code nous avons importé la méthode statique comparant() de la classe Comparator. La méthode comparant() utilise l'expression lambda transmise pour générer un objet Comparator. En d’autres termes, il s’agit également d’une fonction d’ordre supérieur qui accepte une fonction comme paramètre d’entrée et renvoie une autre fonction. En plus de rendre la syntaxe plus concise, un tel code peut également mieux exprimer le problème réel que nous souhaitons résoudre.
Grâce à lui, les comparaisons multiples peuvent devenir plus fluides. Par exemple, le code suivant comparant par nom et par âge dit tout :
Copiez le code comme suit :
final Function<Person, Integer> byAge = personne -> personne.getAge();
final Function<Person, String> byTheirName = personne -> personne.getName();
printPeople("Triés par ordre croissant par âge et nom : ",
personnes.stream()
.sorted(comparant(byAge).thenComparing(byTheirName))
.collect(toList()));
Nous avons d'abord créé deux expressions lambda, l'une renvoie l'âge de la personne spécifiée et l'autre renvoie son nom. Lors de l’appel de la méthode sorted(), nous combinons ces deux expressions afin que plusieurs attributs puissent être comparés. La méthode comparant() crée et renvoie un comparateur basé sur l'âge. Nous appelons ensuite la méthode thenComparing() sur le comparateur renvoyé pour créer un comparateur combiné qui compare l'âge et le nom. Le résultat ci-dessous est le résultat d’un tri d’abord par âge, puis par nom.
Copiez le code comme suit :
Classés par ordre croissant d'âge et de nom :
Jean - 20
Jeanne - 21
Sarah - 21
Grégory - 35
Comme vous pouvez le constater, l'implémentation de Comparator peut être facilement combinée à l'aide d'expressions lambda et des nouvelles classes d'outils fournies par le JDK. Présentons les collectionneurs ci-dessous.