Bonjour ! Dans la leçon précédente, nous avons vu comment tester la levée d'exceptions avec assertThrows. Vous avez remarqué que nous avons utilisé une syntaxe un peu particulière, () -> { ... }, appelée expression lambda. C'était un premier aperçu d'une fonctionnalité puissante introduite dans Java 8.
Aujourd'hui, nous allons plonger au cœur de ce concept. L'objectif de cette leçon est de vous apprendre à écrire des expressions lambda pour implémenter des interfaces fonctionnelles. Nous verrons pourquoi elles existent, comment elles simplifient considérablement le code, et nous maîtriserons leur syntaxe. Cette compétence est fondamentale en Java moderne, car elle est à la base de l'API Stream que nous aborderons très prochainement.
1. Qu'est-ce qu'une interface fonctionnelle ?
Avant de parler des lambdas, il faut comprendre le concept qu'elles servent : l'interface fonctionnelle.
Une interface fonctionnelle est simplement une interface qui ne contient qu'une seule et unique méthode abstraite.
Java en possédait déjà plusieurs avant la version 8, comme :
java.lang.Runnable, avec sa seule méthoderun().java.util.Comparator, avec sa méthodecompare().java.awt.event.ActionListener, avec sa méthodeactionPerformed().
Depuis Java 8, on peut marquer explicitement une interface comme étant fonctionnelle avec l'annotation @FunctionalInterface. Le compilateur vérifiera alors que la règle de la méthode abstraite unique est bien respectée.
@FunctionalInterface
public interface MonInterfaceFonctionnelle {
void executer(); // La seule et unique méthode abstraite
}
2. Le problème : la verbosité des classes anonymes
Avant Java 8, pour utiliser une interface fonctionnelle "à la volée" (sans créer une nouvelle classe nommée juste pour ça), on utilisait une classe anonyme interne. Prenons l'exemple de Runnable pour exécuter du code dans un nouveau thread :
// L'ancienne façon, avec une classe anonyme
Runnable monTraitement = new Runnable() {
@Override
public void run() {
System.out.println("Exécution dans un thread...");
}
};
new Thread(monTraitement).start();
Ce code fonctionne, mais il est très verbeux. Les lignes new Runnable() { ... } et @Override public void run() { ... } constituent du "bruit de fond" (boilerplate). La seule partie vraiment importante est l'instruction System.out.println(...).
Les expressions lambda ont été introduites pour éliminer ce bruit.
3. La solution : les expressions lambda
Une expression lambda permet de fournir directement l'implémentation de l'unique méthode abstraite d'une interface fonctionnelle, de manière beaucoup plus compacte.
Voici le même exemple avec une lambda :
// La nouvelle façon, avec une expression lambda
Runnable monTraitementLambda = () -> System.out.println("Exécution dans un thread...");
new Thread(monTraitementLambda).start();
Le code est plus court, plus clair et va droit au but. Le compilateur Java comprend que () -> ... est une implémentation de la méthode run() de l'interface Runnable, car c'est le contexte d'assignation.
La vidéo suivante explique très bien cette transition d'une classe anonyme vers une expression lambda et explore en détail les différentes syntaxes possibles.
TUTO Java : Expressions Lambdas et références sur méthodes
Cette vidéo de Dominique Liard est une excellente introduction. Elle part d'un exemple concret (la gestion d'événements) pour montrer l'intérêt des lambdas et décortique ensuite leur syntaxe.
Concentrez-vous sur les parties suivantes : De la classe anonyme à la lambda : Observez comment le code utilisant une classe anonyme pour un ActionListener est transformé. L'auteur explique aussi le concept d'interface fonctionnelle. Les variations de syntaxe : Cette partie est cruciale. Elle détaille les différentes manières d'écrire une lambda (avec ou sans parenthèses pour les paramètres, avec ou sans accolades pour le corps, etc.). Prenez le temps de bien comprendre ces nuances.
4. La syntaxe des lambdas en détail
Comme vous l'avez vu dans la vidéo, la syntaxe générale d'une lambda est : (paramètres) -> corps. L'opérateur -> sépare les paramètres du corps de la fonction.
Voici un résumé des règles syntaxiques à retenir.
Cet article de "Développons en Java" par Jean-Michel Doudoux est une référence très complète. Nous allons nous concentrer sur la section qui résume la syntaxe.
Lisez la section 11.2.1. La syntaxe d'une expression lambda. Faites particulièrement attention aux sous-sections sur les paramètres et le corps de l'expression. Elles expliquent : Quand les parenthèses () sont obligatoires ou optionnelles pour les paramètres. Comment les types des paramètres peuvent être inférés par le compilateur. Quand les accolades {} sont nécessaires pour le corps de la lambda. La différence entre un corps qui est une simple expression (retour implicite) et un bloc d'instructions (retour explicite avec return).
Pour synthétiser, voici les cas les plus courants :
| Description | Syntaxe avec lambda | Exemple (Comparator<String>) |
|---|---|---|
| Zéro paramètre | () -> ... |
() -> System.out.println("Action") (pour Runnable) |
| Un paramètre | param -> ... ou (param) -> ... |
s -> s.length() |
| Plusieurs paramètres | (p1, p2) -> ... |
(s1, s2) -> s1.compareTo(s2) |
| Corps en une seule expression | ... -> expression |
(s1, s2) -> s1.length() - s2.length() |
| Corps avec plusieurs instructions | ... -> { instructions; return valeur; } |
(s1, s2) -> { System.out.println("Comparaison"); return s1.compareTo(s2); } |
5. Exercice : Tri d'une liste avec des comparateurs
L'un des usages les plus fréquents des lambdas est de fournir une logique de comparaison pour trier des collections. La méthode sort de l'interface List attend un Comparator, qui est une interface fonctionnelle.
Votre mission :
Dans votre projet, créez une nouvelle classe Main (ou utilisez une classe existante) et copiez-y le code de départ ci-dessous. Votre objectif est d'implémenter les trois tris décrits en utilisant des expressions lambda.
Code de départ :
import java.util.ArrayList;
import java.util.List;
// Vous pouvez réutiliser votre classe Product des leçons précédentes
// Assurez-vous qu'elle a un nom (String) et un prix (double)
// et les getters correspondants.
// Si vous ne l'avez pas, voici un exemple simple :
class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public String toString() {
return "Product{" + "name='" + name + '\'' + ", price=" + price + '}';
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Souris Gamer", 49.99));
products.add(new Product("Clavier Mécanique", 129.50));
products.add(new Product("Écran 27 pouces", 349.00));
products.add(new Product("Tapis de souris XL", 24.90));
// 1. Trier les produits par ordre alphabétique de nom
// VOTRE CODE ICI : products.sort(...);
System.out.println("--- Tri par nom ---");
products.forEach(p -> System.out.println(p));
// 2. Trier les produits par prix croissant
// VOTRE CODE ICI : products.sort(...);
System.out.println("\n--- Tri par prix croissant ---");
products.forEach(p -> System.out.println(p));
// 3. Trier les produits par prix décroissant
// VOTRE CODE ICI : products.sort(...);
System.out.println("\n--- Tri par prix décroissant ---");
products.forEach(p -> System.out.println(p));
}
}
Indices :
- Pour la comparaison de chaînes, vous pouvez utiliser
p1.getName().compareTo(p2.getName()). - Pour la comparaison de
double, qui sont des types primitifs, vous ne pouvez pas utilisercompareTo. Utilisez la soustractionp1.getPrice() - p2.getPrice(). Cependant, la manière la plus sûre est d'utiliserDouble.compare(p1.getPrice(), p2.getPrice()).
Cliquez ici pour voir la solution
// 1. Trier les produits par ordre alphabétique de nom
products.sort((p1, p2) -> p1.getName().compareTo(p2.getName()));
// ...
// 2. Trier les produits par prix croissant
products.sort((p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice()));
// ...
// 3. Trier les produits par prix décroissant
// (Il suffit d'inverser les paramètres de la comparaison)
products.sort((p1, p2) -> Double.compare(p2.getPrice(), p1.getPrice()));
Conclusion
Félicitations ! Vous avez fait un grand pas dans la maîtrise du Java moderne. Les expressions lambda sont un outil que vous utiliserez quotidiennement.
À retenir :
- Une interface fonctionnelle est une interface avec une seule méthode abstraite.
- Une expression lambda est une syntaxe concise pour fournir une implémentation à une interface fonctionnelle.
- La syntaxe de base est
(paramètres) -> corps. Elle est flexible (parenthèses et accolades optionnelles selon le cas). - Les lambdas permettent de passer des comportements (comme une logique de tri) en paramètre de méthodes, rendant le code plus flexible et expressif.
Dans la prochaine leçon, nous allons utiliser cette nouvelle compétence pour explorer l'API Stream. Vous découvrirez comment enchaîner des opérations comme filter, map et collect pour manipuler des collections de données de manière fluide et puissante. Les lambdas que nous avons vues aujourd'hui seront le "carburant" de ces opérations.