Java est un langage orienté objet à la base. Vous avez vu beaucoup d’objets jusqu’à présent.
La programmation fonctionnelle est une façon d’écrire du code de manière plus déclarative. Vous spécifiez ce que vous voulez faire plutôt que de gérer l’état des objets. Vous vous concentrez davantage sur les expressions que sur les boucles.
La programmation fonctionnelle utilise des expressions lambda pour écrire du code. Une expression lambda est un bloc de code qui peut être transmis. Vous pouvez considérer une expression lambda comme une méthode sans nom existant à l’intérieur d’une classe anonyme comme celles que vous avez vues au chapitre “Au-delà des classes”. Elle a des paramètres et un corps comme les méthodes à part entière, mais n’a pas de nom comme une vraie méthode. Les expressions lambda sont souvent appelées lambdas pour faire court. Vous pourriez aussi les connaître sous le nom de closures si Java n’est pas votre premier langage. Si vous avez eu une mauvaise expérience avec les closures dans le passé, ne vous inquiétez pas. Elles sont beaucoup plus simples en Java.
Les lambdas vous permettent d’écrire du code puissant en Java. Dans cette section, nous couvrons un exemple de pourquoi les lambdas sont utiles et la syntaxe des lambdas.
Exemple d’une Lambda
Notre objectif est d’afficher tous les animaux d’une liste selon certains critères. Nous allons vous montrer comment faire cela sans lambdas pour illustrer l’utilité des lambdas. Nous commençons avec l’enregistrement Animal :
public record Animal(String espece, boolean peutSauter, boolean peutNager) { }
L’enregistrement Animal possède trois champs. Disons que nous avons une liste d’animaux et que nous voulons traiter les données en fonction d’un attribut particulier. Par exemple, nous voulons afficher tous les animaux qui peuvent sauter. Nous pouvons définir une interface pour généraliser ce concept et prendre en charge une grande variété de vérifications :
public interface VerifierCaracteristique {
boolean tester(Animal a);
}
La première chose que nous voulons vérifier est si l’Animal peut sauter. Nous fournissons une classe qui implémente notre interface :
public class VerifierSiSauteur implements VerifierCaracteristique {
public boolean tester(Animal a) {
return a.peutSauter();
}
}
Cette classe peut sembler simple, et elle l’est. C’est une partie du problème que les lambdas résolvent. Maintenant, nous avons tout ce dont nous avons besoin pour écrire notre code pour vérifier si un Animal peut sauter :
import java.util.*;
public class RechercheTraditionnelle {
public static void main(String[] args) {
// liste d'animaux
var animaux = new ArrayList();
animaux.add(new Animal("poisson", false, true));
animaux.add(new Animal("kangourou", true, false));
animaux.add(new Animal("lapin", true, false));
animaux.add(new Animal("tortue", false, true));
// passer la classe qui effectue la vérification
afficher(animaux, new VerifierSiSauteur());
}
private static void afficher(List animaux, VerifierCaracteristique verificateur) {
for (Animal animal : animaux) {
// Vérification générale
if (verificateur.tester(animal))
System.out.print(animal + " ");
}
System.out.println();
}
}
La ligne 6 montre la configuration d’un ArrayList avec un type spécifique d’Animal. La méthode afficher() à la ligne 15 est très générale – elle peut vérifier n’importe quelle caractéristique. C’est une bonne conception. Elle ne devrait pas avoir besoin de savoir ce que nous recherchons spécifiquement pour afficher une liste d’animaux.
Que se passe-t-il si nous voulons afficher les Animaux qui nagent ? Soupir. Nous devons écrire une autre classe, VerifierSiNageur. Certes, ce n’est que quelques lignes, mais c’est un tout nouveau fichier. Ensuite, nous devons ajouter une nouvelle ligne sous la ligne 13 qui instancie cette classe. Ce sont deux choses juste pour faire une autre vérification.
Pourquoi ne pouvons-nous pas spécifier la logique dont nous nous soucions directement ici ? Il s’avère que nous le pouvons, avec des expressions lambda. Nous pourrions répéter toute la classe ici et vous faire trouver la seule ligne qui a changé. Au lieu de cela, nous vous montrons simplement que nous pouvons garder la déclaration de notre méthode afficher() inchangée. Remplaçons la ligne 13 par ce qui suit, qui utilise une lambda :
afficher(animaux, a -> a.peutSauter());
Ne vous inquiétez pas si la syntaxe semble un peu bizarre. Vous vous y habituerez, et nous la décrivons dans la section suivante. Nous expliquons également les éléments qui semblent magiques. Pour l’instant, concentrez-vous sur la facilité de lecture. Nous disons à Java que nous nous soucions uniquement de savoir si un Animal peut sauter.
Il ne faut pas beaucoup d’imagination pour comprendre comment nous ajouterions de la logique pour obtenir les Animaux qui peuvent nager. Nous n’avons qu’à ajouter une ligne de code – pas besoin d’une classe supplémentaire pour faire quelque chose de simple. Voici cette autre ligne :
afficher(animaux, a -> a.peutNager());
Et pour les Animaux qui ne peuvent pas nager ?
afficher(animaux, a -> !a.peutNager());
Le point essentiel est qu’il est vraiment facile d’écrire du code qui utilise des lambdas une fois que vous avez mis en place les bases. Ce code utilise un concept appelé exécution différée. L’exécution différée signifie que le code est spécifié maintenant mais sera exécuté plus tard. Dans ce cas, “plus tard” se trouve à l’intérieur du corps de la méthode afficher(), par opposition au moment où il est passé à la méthode.
Apprendre la syntaxe des Lambdas
L’une des expressions lambda les plus simples que vous puissiez écrire est celle que vous venez de voir :
a -> a.peutSauter()
Les lambdas fonctionnent avec des interfaces qui ont exactement une méthode abstraite. Dans ce cas, Java examine l’interface VerifierCaracteristique, qui a une méthode. La lambda dans notre exemple suggère que Java devrait appeler une méthode avec un paramètre Animal qui renvoie une valeur booléenne correspondant au résultat de a.peutSauter(). Nous savons tout cela parce que nous avons écrit le code. Mais comment Java le sait-il ?
Java s’appuie sur le contexte pour déterminer ce que signifient les expressions lambda. Le contexte fait référence à où et comment la lambda est interprétée. Par exemple, si nous voyons quelqu’un dans la file d’attente pour entrer au zoo et qu’il a son portefeuille sorti, il est juste de supposer qu’il veut acheter des billets pour le zoo. Alternativement, s’ils sont dans la file d’attente de la concession avec leur portefeuille sorti, ils ont probablement faim.
En se référant à notre exemple précédent, nous avons passé la lambda comme deuxième paramètre de la méthode afficher() :
afficher(animaux, a -> a.peutSauter());
La méthode afficher() attend un VerifierCaracteristique comme deuxième paramètre :
private static void afficher(List animaux, VerifierCaracteristique verificateur) { … }
Puisque nous passons une lambda à la place, Java essaie de faire correspondre notre lambda à la déclaration de méthode abstraite dans l’interface VerifierCaracteristique :
boolean tester(Animal a);
Puisque la méthode de cette interface prend un Animal, le paramètre lambda doit être un Animal. Et puisque la méthode de cette interface renvoie un booléen, nous savons que la lambda renvoie un booléen.
La syntaxe des lambdas est délicate car de nombreuses parties sont facultatives. Ces deux lignes font exactement la même chose :
a -> a.peutSauter()
(Animal a) -> { return a.peutSauter(); }
Examinons ce qui se passe ici. Le premier exemple a trois parties :
- Un seul paramètre spécifié avec le nom a
- L’opérateur flèche (->) pour séparer le paramètre et le corps
- Un corps qui appelle une seule méthode et renvoie le résultat de cette méthode
Le deuxième exemple montre la forme la plus verbeuse d’une lambda qui renvoie un booléen :
- Un seul paramètre spécifié avec le nom a et indiquant que le type est Animal
- L’opérateur flèche (->) pour séparer le paramètre et le corps
- Un corps qui contient une ou plusieurs lignes de code, y compris un point-virgule et une instruction return
Les parenthèses autour des paramètres lambda peuvent être omises uniquement s’il y a un seul paramètre et que son type n’est pas explicitement indiqué. Java fait cela parce que les développeurs utilisent couramment des expressions lambda de cette manière et peuvent taper le moins possible.
Il ne devrait pas être nouveau pour vous que nous puissions omettre les accolades lorsque nous n’avons qu’une seule instruction. Nous l’avons déjà fait avec les instructions if et les boucles. Java vous permet d’omettre l’instruction return et le point-virgule (;) lorsque aucune accolade n’est utilisée. Ce raccourci spécial ne fonctionne pas lorsque vous avez deux instructions ou plus. Au moins, c’est cohérent avec l’utilisation de {} pour créer des blocs de code ailleurs.
La syntaxe des Figures 8.1 et 8.2 peut être mélangée et assortie. Par exemple, les suivantes sont valides :
a -> { return a.peutSauter(); }
(Animal a) -> a.peutSauter()
Voici un fait amusant : s -> {} est une lambda valide. S’il n’y a pas de code du côté droit de l’expression, vous n’avez pas besoin du point-virgule ou de l’instruction return.
Le tableau 8.1 montre des exemples de lambdas valides qui renvoient un booléen.
Lambda | Nombre de paramètres |
---|---|
() -> true | 0 |
x -> x.commenceAvec(“test”) | 1 |
(String x) -> x.commenceAvec(“test”) | 1 |
(x, y) -> { return x.commenceAvec(“test”); } | 2 |
(String x, String y) -> x.commenceAvec(“test”) | 2 |
La première ligne prend zéro paramètre et renvoie toujours la valeur booléenne true. La deuxième ligne prend un paramètre et appelle une méthode dessus, renvoyant le résultat. La troisième ligne fait la même chose, sauf qu’elle définit explicitement le type de la variable. Les deux dernières lignes prennent deux paramètres et en ignorent un – il n’y a pas de règle qui dit que vous devez utiliser tous les paramètres définis.
Maintenant, assurons-nous que vous pouvez identifier la syntaxe invalide pour chaque ligne du tableau 8.2, où chaque lambda est censée renvoyer un booléen. Assurez-vous de comprendre ce qui ne va pas avec ces exemples.
Lambda invalide | Raison |
---|---|
x, y -> x.commenceAvec(“poisson”) | Parenthèses manquantes à gauche |
x -> { x.commenceAvec(“chameau”); } | Return manquant à droite |
x -> { return x.commenceAvec(“girafe”) } | Point-virgule manquant à l’intérieur des accolades |
String x -> x.terminePar(“aigle”) | Parenthèses manquantes à gauche |
N’oubliez pas que les parenthèses sont facultatives uniquement lorsqu’il y a un paramètre et qu’il n’a pas de type déclaré. Ce sont les bases de l’écriture d’une lambda. À la fin du chapitre, nous couvrons des règles supplémentaires concernant l’utilisation des variables dans une lambda.
Attribution de Lambdas à var
Pourquoi pensez-vous que cette ligne de code ne compile pas ?
var invalide = (Animal a) -> a.peutSauter(); // NE COMPILE PAS
Souvenez-vous quand nous avons parlé de Java déduisant des informations sur la lambda à partir du contexte ? Eh bien, var suppose également le type en fonction du contexte. Il n’y a pas assez de contexte ici ! Ni la lambda ni var n’ont suffisamment d’informations pour déterminer quel type d’interface fonctionnelle devrait être utilisé.