↑
CHAPITRE 11.2

Expressions lambda

Simplifier le code avec les expressions lambda (Java 8+)
Les expressions lambda permettent d'écrire du code plus concis et fonctionnel. Introduites en Java 8, elles sont particulièrement utiles avec les collections et les streams.

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) -> expression ou (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
Conseils pratiques :
  • 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