L'objectif de cet article est de démystifier un peu les lambdas en Java. Vous montrez que finalement ce n'est pas grand chose à part un peu de simplification de syntaxe qu'on a appliqué à beaucoup de cas.
Si on revient à l'ancien temps (enfin pas si lointain que ça, ça fait à peine quatre ans que Java 8 est sorti (mars 2014)), les lambdas n'existaient pas, mais on faisait déjà tout sans elles.
Par exemple, disons que l'on souhaite manipuler une liste de personnage de série. Pouvoir les trier de différente façons, pouvoir n'afficher que ceux qui ont certains, etc. On va commencer par définir notre structure puis on va travailler sur la liste de nos données.
Voici la structure qu'on utilisera tout au long de l'article :
// Character.java
public class Character {
private String name;
private int apparitionYear;
private String actor;
private String show;
// ...
// constructor
// getter/setter
// toString
}
Et la liste de personnage qu'on utilisera :
List<Character> characters = Arrays.asList(new Character[] {
new Character("First Doctor", 1963, "William Hartnell", MALE, "classic"),
new Character("Second Doctor", 1966, "Patrick Troughton", MALE, "classic"),
new Character("Third Doctor", 1970, "Jon Pertwee", MALE, "classic"),
new Character("Fourth Doctor", 1974, "Tom Baker", MALE, "classic"),
new Character("Fifth Doctor", 1981, "Peter Davison", MALE, "classic"),
new Character("Sixth Doctor", 1984, "Colin Baker", MALE, "classic"),
new Character("Seventh Doctor", 1987, "Sylvester McCoy", MALE, "classic"),
new Character("Eighth Doctor", 1996, "Paul McGann", MALE, "TV film"),
new Character("War Doctor", 2013, "John Hurt", MALE, "2005 show"),
new Character("Ninth Doctor", 2005, "Christopher Eccleston", MALE, "2005 show"),
new Character("Tenth Doctor", 2005, "David Tennant", MALE, "2005 show"),
new Character("Eleventh Doctor", 2010, "Matt Smith", MALE, "2005 show"),
new Character("Twelfth Doctor", 2013, "Peter Capaldi", MALE, "2005 show"),
new Character("Thirteenth Doctor", 2017, "Jodie Whittaker", FEMALE, "2005 show"),
new Character("River Song", 2008, "Alex Kingston", FEMALE, "2005 show"),
new Character("Jack Harkness", 2005, "John Barrowman", MALE, "2005 show"),
new Character("Rose Tyler", 2005, "Billie Piper", FEMALE, "2005 show"),
new Character("Martha Jones", 2007, "Freema Agyeman", FEMALE, "2005 show"),
new Character("Donna Noble", 2006, "Catherine Tate", FEMALE, "2005 show"),
new Character("Amelia « Amy » Pond", 2010, "Karen Gillan", FEMALE, "2005 show"),
new Character("Clara Oswald", 2010, "Jenna Coleman", FEMALE, "2005 show"),
new Character("Nardole", 2015, "Matt Lucas", MALE, "2005 show"),
new Character("Bill Potts", 2017, "Pearl Mackie", FEMALE, "2005 show")
});
Maintenant passons aux choses sérieuses et jouons un peu avec ces données !
Au besoin, les sources Java sont disponibles ici : https://gist.github.com/kuroidoruido/5454954cad1133e9ce0a568f28525b74.
Et si on affichait tout ça ?
La première chose à faire c'est de vérifier que toutes nos données sont correctes. Et pour ça on va afficher toutes les données en utilisant la méthode toString() de Character.
En Java 7 : avec un foreach
for(Character c : characters) {
System.out.println(c);
}
Ici on n'appelle pas directement toString() parce qu'on sait que println() attend une String en paramètre et que l'appelle à toString() est fait implicitement dans ce cas là.
En Java 8 : la méthode forEach()
La transformation la plus basique est de simplement passer par la méthode .forEach() de l'interface Iterable. Cette méthode attend une instance de Consumer qui va simplement être capable de traiter un par les éléments de la liste.
Version sans lambda :
characters.forEach(new Consumer<Character>() {
public void accept(Character c) {
System.out.println(c);
}
});
Ici on crée une instance de l'interface Consumer en même temps que l'on redéfinit le comportement de la méthode accept() en passant par une classe abstraite. On voit donc que trois concepts ressorte fortement : implémentation d'une interface, création d'une classe abstraite et surcharge de méthode.
Version avec lambda :
characters.forEach(c -> { System.out.println(c); });
// or
characters.forEach(c -> System.out.println(c));
Ici on utilise une lambda pour faire exactement la même chose que pour la version sans lambda, mais on se concentre sur l'essentiel : passer chaque personnage à la méthode println().
À noter que les deux versions (avec et sans accolades) sont strictements equivalentes. Pouvoir omettre les accolades est un sucre syntaxique offert par Java pour ce genre de cas où on effectue une seule et unique action et où la valeur de retour de cette action est de type compatible avec le type attendu en retour de la lambda (ici Concumer.accept() renvoi void, et System.out.println() aussi, donc il n'y a pas de problème).
Version avec référence de méthode :
characters.forEach(System.out::println);
Dans cette dernière variante, on utilise une référence de méthode. Dans version avec lambda, tout ce que faisait notre lambda c'est prendre le paramètre et le passer en paramètre d'une fonction directement. Dans ce cas de figure on peut donc directement omettre la lambda et indiquer à la méthode forEach() de passer directement la valeur à println().
Trier toutes ces données
Maintenant que l'on a vu comment afficher les données, on va essayer de trier les personnages pour les avoir non pas dans l'ordre d'insertion, mais dans l'ordre alphabétique et aussi par ordre d'apparition dans la série (deux affichages différents).
Version Java 7 : l'interface Comparator<T>
System.out.println("------ Alphabetical Order");
Collections.sort(characters,new Comparator<Character>() {
@Override
public int compare(Character c1, Character c2) {
return c1.getName().compareTo(c2.getName());
}
});
characters.forEach(System.out::println);
System.out.println("------ Apparition Order");
Collections.sort(characters,new Comparator<Character>() {
@Override
public int compare(Character c1, Character c2) {
return c1.getApparitionYear() - c2.getApparitionYear();
}
});
characters.forEach(System.out::println);
Comme précédemment on voit qu'on est ici obligé de passer par une classe abstraite pour redéfinir la méthode compare() de Comparator. Dans notre cas on le fait deux fois, ce qui rend le code peu lisible et très verbeux.
À noter que je n'ai pas non plus utilisé la méthode sort() de List (introduite en Java 8) (ce que je fais pour la version lambda) afin d'indiquer le code qu'on retrouve le plus souvent et qui correspond à ce qu'il était possible de faire en Java 7.
Version Java 8 : lambda
System.out.println("------ Alphabetical Order");
final Comparator<Character> compAlphaName = (c1,c2) -> c1.getName().compareTo(c2.getName());
characters.sort(compAlphaName);
characters.forEach(System.out::println);
System.out.println("------ Apparition Order");
characters.sort((c1,c2) -> c1.getApparitionYear() - c2.getApparitionYear());
characters.forEach(System.out::println);
On voit que le code est beaucoup plus concis et se concentre sur l'essentiel : ce que l'on compare et l'affichage. J'ai profité de cet exemple pour montrer que l'on peut affecter la lambda à une variable comme une instance de Comparator, ce qui pourrait permettre de l'utiliser plusieurs fois.
Version Java 8 : stream et immutabilité
System.out.println("------ Alphabetical Order");
characters.stream()
.sorted((c1,c2) -> c1.getName().compareTo(c2.getName()))
.forEach(System.out::println);
System.out.println("------ Apparition Order");
characters.stream()
.sorted((c1,c2) -> c1.getApparitionYear() - c2.getApparitionYear())
.forEach(System.out::println);
Dans cette dernière version, on affiche bien l'ensemble des personnages deux fois, une fois pour chaque ordre choisi, mais cette fois-ci on passe par un Stream pour ne pas avoir à modifier la liste d'origine. Si on regardait l'ordre après application de cette version, l'ordre serait le même que celui d'origine.
Affichage partiel et filtres
Jusque là on faisait un traitement très simple, ici on va passer à un affichage un peu plus complexe : on ne va afficher que le nom du personnage et l'information sur laquelle on fera un filtre. Comme pour l'exemple précédent on fera deux affichages : un affichage des personnages qui sont des femmes, et un affichage des personnages apparu entre 2005 et 2008.
Version Java 7
System.out.println("------ Female characters");
for(Character c : characters) {
if(c.getGender() == FEMALE) {
System.out.println(c.getName() + " " + c.getGender());
}
}
System.out.println("------ Characters appeared between 2005 and 2008");
for(Character c : characters) {
if(2005 <= c.getApparitionYear() && c.getApparitionYear() <= 2008) {
System.out.println(c.getName() + " " + c.getApparitionYear());
}
}
Version Java 8 : simple Stream
System.out.println("------ Female characters");
characters.stream()
.filter(c -> c.getGender() == FEMALE)
.forEach(c -> System.out.println(c.getName() + " " + c.getGender()));
System.out.println("------ Characters appeared between 2005 and 2008");
characters.stream()
.filter(c -> 2005 <= c.getApparitionYear() && c.getApparitionYear() <= 2008)
.forEach(c -> System.out.println(c.getName() + " " + c.getApparitionYear()));
Le code est assez simple à comprendre à la lecture. Je pointe seulement que la méthode filter() prend en paramètre une instance de Precidate, qui est une interface qui propose la méthode test() qui renvoie un boolean.
Version Java 8 : Stream et map
System.out.println("------ Female characters");
characters.stream()
.filter(c -> c.getGender() == FEMALE)
.map(c -> c.getName() + " " + c.getGender())
.forEach(System.out::println);
System.out.println("------ Characters appeared between 2005 and 2008");
characters.stream()
.filter(c -> 2005 <= c.getApparitionYear())
.filter(c -> c.getApparitionYear() <= 2008)
.map(c -> c.getName() + " " + c.getApparitionYear())
.forEach(System.out::println);
Dans cette version deux éléments sont à noter. Le premier est l'utilisation de la méthode map() qui permet de "transformer" l'objet Character en une String contenant uniquement ce dont on a besoin. Le second est l'enchaînement de deux appels à filter(), qui sont équivalent à l'écriture avec deux conditions séparés par un ET logique (&&) de la version précédente. J'en ai aussi profité pour passer de nouveau par une référence de méthode, car j'aime beaucoup cette syntaxe.
Filtre, Tri et affichage partiel
Cette fois ci, on va mélanger un peu tout ce que l'on a vu pour avoir de nouveau deux affichages, les deux mêmes que dans la partie précédente, mais cette fois ci en ajoutant un tri pour le premier affichage par nom et le second par année d'apparition. Et comme on peut le faire on va aussi faire un troisème affichage qui recoupe les deux précédents (sans recalculer les deux deux listes pour la troisième), c'est-à-dire afficher les femmes qui sont apparue la première fois entre 2005 et 2008 trié par apparition.
Version Java 7
System.out.println("------ Female characters");
final Comparator<Character> compAlphaName = new Comparator<Character>() {
@Override
public int compare(Character c1, Character c2) {
return c1.getName().compareTo(c2.getName());
}
};
final List<Character> femaleCharacters = new ArrayList<>();
characters.sort(compAlphaName);
for(Character c : characters) {
if(c.getGender() == FEMALE) {
femaleCharacters.add(c);
System.out.println(c.getName() + " " + c.getGender());
}
}
System.out.println("------ Characters appeared between 2005 and 2008");
final Comparator<Character> compAppearanceYear = new Comparator<Character>() {
@Override
public int compare(Character c1, Character c2) {
return c1.getAppearanceYear() - c2.getAppearanceYear();
}
};
final List<Character> charactersAppearedBetween2005and2008 = new ArrayList<>();
characters.sort(compAppearanceYear);
for(Character c : characters) {
if(2005 <= c.getAppearanceYear() && c.getAppearanceYear() <= 2008) {
charactersAppearedBetween2005and2008.add(c);
System.out.println(c.getName() + " " + c.getAppearanceYear());
}
}
System.out.println("------ Female characters appeared between 2005 and 2008");
femaleCharacters.retainAll(charactersAppearedBetween2005and2008);
femaleCharacters.sort(compAppearanceYear);
for(Character c : femaleCharacters) {
System.out.println(c.getAppearanceYear() + " " + c.getName() + " " + c.getGender());
}
Version Java 8
System.out.println("------ Female characters");
final Comparator<Character> compAlphaName = (c1,c2) -> c1.getName().compareTo(c2.getName());
final List<Character> femaleCharacters = characters.stream()
.filter(c -> c.getGender() == FEMALE)
.collect(Collectors.toList());
femaleCharacters.stream()
.sorted(compAlphaName)
.forEach(c -> System.out.println(c.getName() + " " + c.getGender()));
System.out.println("------ Characters appeared between 2005 and 2008");
final Comparator<Character> compAppearanceYear =
(c1,c2) -> c1.getAppearanceYear() - c2.getAppearanceYear();
final List<Character> charactersAppearedBetween2005and2008 = characters.stream()
.filter(c -> 2005 <= c.getAppearanceYear() && c.getAppearanceYear() <= 2008)
.collect(Collectors.toList());
charactersAppearedBetween2005and2008.stream()
.sorted(compAppearanceYear)
.forEach(c -> System.out.println(c.getName() + " " + c.getAppearanceYear()));
System.out.println("------ Female characters appeared between 2005 and 2008");
charactersAppearedBetween2005and2008.stream()
.filter(c -> femaleCharacters.contains(c))
.sorted(compAppearanceYear)
.forEach(c -> System.out.println(
c.getAppearanceYear() + " " + c.getName() + " " + c.getGender()));
Je n'ai pas de vrai commentaire sur ces deux versions. À vous de juger quelle version semble la plus lisible et maintenable des deux.
Conclusion
J'espère avoir réussi à faire un parallèle au moins correct entre la syntaxe Java 7 et Java 8 entre ces quelques exemples. Je pense faire un autre article sur map() et reduce(), qui permettent de faire des traitements très puissants sur des collections.
Si je n'ai pas été clair ou que j'ai fait des erreurs, n'hésitez pas à me contacter sur Twitter.
Crédit Photo : https://pixabay.com/en/productivity-work-businessman-1995786/