11.2Expressions lambda
11.2.1 – Syntaxe générale
Les expressions lambda (introduites en Java 8) permettent d'écrire du code plus concis et fonctionnel. Elles remplacent souvent les classes anonymes pour des interfaces fonctionnelles et permettent un style de programmation plus moderne et expressif.
🔑 Syntaxe de base
Syntaxe générale : (paramètres) -> { corps } ou (paramètres) -> expression
1. Lambda sans paramètres
Exemple : Lambda sans paramètres
// Ancienne syntaxe (classe anonyme)
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Avec lambda (équivalent)
Runnable r2 = () -> System.out.println("Hello");
// Lambda avec plusieurs instructions (accolades nécessaires)
Runnable r3 = () -> {
System.out.println("Hello");
System.out.println("World");
};
2. Lambda avec un paramètre
Exemple : Lambda avec un paramètre
// Interface fonctionnelle
interface Affichage {
void afficher(String message);
}
// Syntaxe complète
Affichage a1 = (String message) -> System.out.println(message);
// Syntaxe simplifiée (type inféré)
Affichage a2 = (message) -> System.out.println(message);
// Syntaxe ultra-simplifiée (un seul paramètre, parenthèses optionnelles)
Affichage a3 = message -> System.out.println(message);
// Avec plusieurs instructions
Affichage a4 = message -> {
System.out.println("Message reçu : " + message);
System.out.println("Longueur : " + message.length());
};
3. Lambda avec plusieurs paramètres
Exemple : Lambda avec plusieurs paramètres
// Interface fonctionnelle
interface Calcul {
int calculer(int a, int b);
}
// Syntaxe complète avec types
Calcul c1 = (int a, int b) -> a + b;
// Syntaxe simplifiée (types inférés)
Calcul c2 = (a, b) -> a + b;
// Avec return explicite (accolades nécessaires)
Calcul c3 = (a, b) -> {
int resultat = a + b;
return resultat;
};
// Opérations complexes
Calcul c4 = (a, b) -> {
if (a > b) {
return a * b;
} else {
return a + b;
}
};
4. Lambda avec expression simple vs bloc
Exemple : Expression simple vs bloc
interface Operation {
int appliquer(int x);
}
// Expression simple (pas d'accolades, return implicite)
Operation carre = x -> x * x;
// Bloc avec accolades (return explicite requis)
Operation carre2 = x -> {
int resultat = x * x;
return resultat;
};
// Expression simple avec opération complexe
Operation absolu = x -> x < 0 ? -x : x;
đź“‹ Interfaces fonctionnelles de base
Java 8 fournit plusieurs interfaces fonctionnelles dans le package java.util.function. Voici les principales :
1. Consumer<T> - Consomme un argument, ne retourne rien
Exemple : Consumer
import java.util.function.Consumer;
// Consumer : prend un argument, ne retourne rien
Consumer<String> afficher = s -> System.out.println(s);
afficher.accept("Hello"); // Affiche : Hello
// Consumer avec plusieurs instructions
Consumer<Integer> afficherCarre = n -> {
int carre = n * n;
System.out.println("Le carré de " + n + " est " + carre);
};
afficherCarre.accept(5); // Affiche : Le carré de 5 est 25
2. Supplier<T> - Ne prend pas d'argument, retourne une valeur
Exemple : Supplier
import java.util.function.Supplier;
// Supplier : ne prend pas d'argument, retourne une valeur
Supplier<String> obtenirMessage = () -> "Hello World";
String message = obtenirMessage.get(); // "Hello World"
// Supplier avec génération aléatoire
Supplier<Integer> nombreAleatoire = () -> (int)(Math.random() * 100);
int nombre = nombreAleatoire.get();
3. Function<T, R> - Prend un argument, retourne une valeur
Exemple : Function
import java.util.function.Function;
// Function : prend un argument de type T, retourne un type R
Function<String, Integer> longueur = s -> s.length();
int len = longueur.apply("Hello"); // 5
// Function avec transformation
Function<Integer, String> convertir = n -> "Nombre : " + n;
String resultat = convertir.apply(42); // "Nombre : 42"
4. Predicate<T> - Prend un argument, retourne un boolean
Exemple : Predicate
import java.util.function.Predicate;
// Predicate : prend un argument, retourne un boolean
Predicate<Integer> estPair = n -> n % 2 == 0;
boolean resultat = estPair.test(4); // true
// Predicate avec conditions complexes
Predicate<String> estLong = s -> s.length() > 10;
boolean estLongue = estLong.test("Hello World"); // true
5. BiFunction<T, U, R> - Prend deux arguments, retourne une valeur
Exemple : BiFunction
import java.util.function.BiFunction;
// BiFunction : prend deux arguments, retourne une valeur
BiFunction<Integer, Integer, Integer> addition = (a, b) -> a + b;
int somme = addition.apply(5, 3); // 8
// BiFunction avec types différents
BiFunction<String, Integer, String> repeter = (s, n) -> s.repeat(n);
String resultat = repeter.apply("Hi", 3); // "HiHiHi"
6. Runnable - Pour les threads
Exemple : Runnable
// Runnable : ne prend pas d'argument, ne retourne rien
Runnable tache = () -> System.out.println("Exécution dans un thread");
// Utilisation avec Thread
Thread thread = new Thread(tache);
thread.start();
// Ou directement
new Thread(() -> System.out.println("Thread lambda")).start();
7. Comparator<T> - Pour le tri
Exemple : Comparator
import java.util.Comparator;
import java.util.Arrays;
import java.util.List;
List<String> noms = Arrays.asList("Alice", "Bob", "Charlie");
// Comparator avec lambda
Comparator<String> parLongueur = (s1, s2) -> s1.length() - s2.length();
noms.sort(parLongueur);
// Ou directement
noms.sort((s1, s2) -> s1.length() - s2.length());
// Comparator inversé
noms.sort((s1, s2) -> s2.length() - s1.length());
💡 Règles de syntaxe
- Parenthèses : Obligatoires pour 0 ou 2+ paramètres, optionnelles pour 1 paramètre
- Types : Optionnels (inférés automatiquement)
- Accolades : Obligatoires si plusieurs instructions ou return explicite
- Return : Implicite avec expression simple, explicite avec bloc
11.2.2 – Cas d'usage courants
Les lambdas sont particulièrement utiles avec les collections, les streams, les threads, et bien d'autres cas d'usage. Voici tous les cas possibles :
🔄 Avec les collections
1. forEach - Parcourir une collection
Exemple : forEach avec List
import java.util.ArrayList;
import java.util.List;
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add("Charlie");
// Parcourir avec lambda
list.forEach(n -> System.out.println(n));
// Avec référence de méthode
list.forEach(System.out::println);
// Avec plusieurs instructions
list.forEach(n -> {
System.out.println("Nom : " + n);
System.out.println("Longueur : " + n.length());
});
2. removeIf - Supprimer des éléments
Exemple : removeIf avec Predicate
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add("Charlie");
// Supprimer les éléments qui commencent par "A"
list.removeIf(s -> s.startsWith("A"));
// Supprimer les éléments de longueur > 4
list.removeIf(s -> s.length() > 4);
3. replaceAll - Remplacer tous les éléments
Exemple : replaceAll
List<String> list = new ArrayList<>();
list.add("alice");
list.add("bob");
// Mettre en majuscules
list.replaceAll(s -> s.toUpperCase());
// Ajouter un préfixe
list.replaceAll(s -> "Nom: " + s);
4. sort - Trier avec Comparator
Exemple : sort avec lambda
List<String> list = new ArrayList<>();
list.add("Charlie");
list.add("Alice");
list.add("Bob");
// Trier par ordre alphabétique
list.sort((s1, s2) -> s1.compareTo(s2));
// Trier par longueur
list.sort((s1, s2) -> s1.length() - s2.length());
// Trier inversé
list.sort((s1, s2) -> s2.compareTo(s1));
🌊 Avec les streams
1. filter - Filtrer les éléments
Exemple : filter avec Predicate
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
List<Integer> nombres = Arrays.asList(1, 2, 3, 4, 5, 10, 15, 20);
// Filtrer les nombres pairs
List<Integer> pairs = nombres.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// Filtrer les nombres > 10
List<Integer> grands = nombres.stream()
.filter(n -> n > 10)
.collect(Collectors.toList());
// Filtrer avec condition complexe
List<Integer> resultat = nombres.stream()
.filter(n -> n > 5 && n < 15)
.collect(Collectors.toList());
2. map - Transformer les éléments
Exemple : map avec Function
List<Integer> nombres = Arrays.asList(1, 2, 3, 4, 5);
// Calculer les carrés
List<Integer> carres = nombres.stream()
.map(n -> n * n)
.collect(Collectors.toList());
// Transformer en String
List<String> chaines = nombres.stream()
.map(n -> "Nombre: " + n)
.collect(Collectors.toList());
// Transformer avec opération complexe
List<Integer> doubles = nombres.stream()
.map(n -> {
int resultat = n * 2;
return resultat + 1;
})
.collect(Collectors.toList());
3. forEach - Parcourir un stream
Exemple : forEach sur stream
List<String> list = Arrays.asList("Alice", "Bob", "Charlie");
// Afficher chaque élément
list.stream().forEach(s -> System.out.println(s));
// Avec traitement
list.stream().forEach(s -> {
String maj = s.toUpperCase();
System.out.println("Nom en majuscules : " + maj);
});
4. reduce - Réduire à une valeur
Exemple : reduce
List<Integer> nombres = Arrays.asList(1, 2, 3, 4, 5);
// Somme
int somme = nombres.stream()
.reduce(0, (a, b) -> a + b);
// Produit
int produit = nombres.stream()
.reduce(1, (a, b) -> a * b);
// Maximum
int max = nombres.stream()
.reduce(Integer.MIN_VALUE, (a, b) -> a > b ? a : b);
5. allMatch, anyMatch, noneMatch - Tests conditionnels
Exemple : Tests avec Predicate
List<Integer> nombres = Arrays.asList(2, 4, 6, 8);
// Tous pairs ?
boolean tousPairs = nombres.stream()
.allMatch(n -> n % 2 == 0); // true
// Au moins un > 10 ?
boolean auMoinsUn = nombres.stream()
.anyMatch(n -> n > 10); // false
// Aucun négatif ?
boolean aucunNegatif = nombres.stream()
.noneMatch(n -> n < 0); // true
6. findFirst, findAny - Trouver un élément
Exemple : findFirst et findAny
import java.util.Optional;
List<Integer> nombres = Arrays.asList(1, 2, 3, 4, 5, 10, 15);
// Trouver le premier > 5
Optional<Integer> premier = nombres.stream()
.filter(n -> n > 5)
.findFirst();
premier.ifPresent(n -> System.out.println("Premier : " + n));
// Trouver n'importe quel élément pair
Optional<Integer> pair = nombres.stream()
.filter(n -> n % 2 == 0)
.findAny();
đź§µ Avec les threads
Exemple : Threads avec lambda
// Thread simple
Thread t1 = new Thread(() -> System.out.println("Thread 1"));
t1.start();
// Thread avec plusieurs instructions
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2 : " + i);
}
});
t2.start();
// ExecutorService avec lambda
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> System.out.println("Tâche exécutée"));
executor.shutdown();
🎯 Références de méthodes
Les références de méthodes sont une syntaxe encore plus concise que les lambdas dans certains cas.
1. Référence à une méthode statique
Exemple : Référence à méthode statique
import java.util.function.Function;
// Lambda
Function<String, Integer> f1 = s -> Integer.parseInt(s);
// Référence de méthode
Function<String, Integer> f2 = Integer::parseInt;
// Utilisation
int nombre = f2.apply("123");
2. Référence à une méthode d'instance
Exemple : Référence à méthode d'instance
List<String> list = Arrays.asList("alice", "bob");
// Lambda
list.forEach(s -> System.out.println(s));
// Référence de méthode
list.forEach(System.out::println);
// Méthode sur l'objet
list.forEach(String::toUpperCase); // Appelle toUpperCase() sur chaque élément
3. Référence à un constructeur
Exemple : Référence à constructeur
import java.util.function.Supplier;
// Lambda
Supplier<StringBuilder> s1 = () -> new StringBuilder();
// Référence de méthode
Supplier<StringBuilder> s2 = StringBuilder::new;
// Avec Function
Function<String, StringBuilder> f = StringBuilder::new;
StringBuilder sb = f.apply("Hello");
đź”— Lambdas avec variables locales
Exemple : Accès aux variables locales
// Variable final ou effectivement final
final int multiplicateur = 10;
List<Integer> nombres = Arrays.asList(1, 2, 3);
// Lambda peut accéder à multiplicateur
List<Integer> resultats = nombres.stream()
.map(n -> n * multiplicateur)
.collect(Collectors.toList());
// Variable effectivement final (pas modifiée après)
int facteur = 5;
List<Integer> resultats2 = nombres.stream()
.map(n -> n * facteur) // OK : facteur est effectivement final
.collect(Collectors.toList());
// ❌ ERREUR : variable modifiée
int compteur = 0;
// nombres.forEach(n -> compteur++); // Erreur : compteur n'est pas final
📊 Tableau récapitulatif des interfaces fonctionnelles
| Interface | Méthode | Paramètres | Retour | Exemple |
|---|---|---|---|---|
| Consumer<T> | accept(T) | 1 | void | x -> System.out.println(x) |
| Supplier<T> | get() | 0 | T | () -> "Hello" |
| Function<T, R> | apply(T) | 1 | R | x -> x * 2 |
| Predicate<T> | test(T) | 1 | boolean | x -> x > 10 |
| BiFunction<T, U, R> | apply(T, U) | 2 | R | (a, b) -> a + b |
| Runnable | run() | 0 | void | () -> System.out.println("Hi") |
| Comparator<T> | compare(T, T) | 2 | int | (a, b) -> a.compareTo(b) |
💡 Points clés à retenir
- Syntaxe :
(paramètres) -> expressionou(paramètres) -> { bloc } - Interfaces fonctionnelles : Une seule méthode abstraite
- Collections : forEach, removeIf, replaceAll, sort
- Streams : filter, map, reduce, forEach, allMatch, anyMatch, findFirst
- Threads : Runnable avec lambda
- Références de méthodes : Syntaxe encore plus concise (::)
- Variables locales : Doivent ĂŞtre final ou effectivement final
- Types inférés : Le compilateur déduit automatiquement les types
- Utilisez les lambdas pour simplifier le code avec les collections et streams
- Préférez les références de méthodes quand c'est possible (plus concis)
- Les lambdas améliorent la lisibilité du code fonctionnel
- Attention : les variables capturées doivent être final ou effectivement final
- Les lambdas sont particulièrement utiles avec les streams pour le traitement de données