1. Le Changement de Paradigme : Impératif vs. Déclaratif
Jusqu'à présent, vous avez écrit du code de manière impérative. C'est comme écrire une recette de cuisine détaillée : vous décrivez comment
Style Impératif (AVANT)
On explique le "comment"
List<String> nomsEnMaj = new ArrayList<>();
for (String nom : noms) {
if (nom.length() > 4) {
nomsEnMaj.add(nom.toUpperCase());
}
}
Style Déclaratif (APRÈS)
On décrit le "quoi"
List<String> nomsEnMaj = noms.stream()
.filter(n -> n.length() > 4)
.map(String::toUpperCase)
.collect(Collectors.toList());
Analogie : Impératif vs. Déclaratif
Impératif : Comme commander un plat en donnant les instructions au cuisinier : "Prends des tomates, coupe-les, ajoute du sel, mélange..."
Déclaratif : Comme commander un plat au restaurant : "Je veux une salade tomate-mozzarella", sans expliquer comment la préparer.
L'API Streams introduit un style déclaratif qui est non seulement plus court, mais aussi beaucoup plus expressif et lisible une fois maîtrisé.
2. Les Expressions Lambda : Des Fonctions sur le Vif
Le premier ingrédient de cette nouvelle façon de coder est l'expression Lambda. C'est une manière courte d'écrire une fonction anonyme que vous pouvez passer en paramètre à d'autres méthodes.
Anatomie d'une Expression Lambda
Une lambda se compose de trois parties :
Syntaxe Lambda
(paramètres) -> { corps }
- (paramètres) : La liste des entrées (comme pour une méthode)
- -> : La "flèche" qui sépare paramètres et corps
- { corps } : L'instruction à exécuter (accolades optionnelles si une seule ligne)
Exemples de Lambdas
// 1. Lambda affichant un message
(String nom) -> System.out.println("Bonjour, " + nom);
// 2. Lambda retournant la somme de deux entiers
(int a, int b) -> { return a + b; }
// 3. Version simplifiée (return implicite)
(a, b) -> a + b
// 4. Lambda sans paramètre
() -> System.out.println("Pas de paramètres !")
Simplifions
Si le corps ne contient qu'une seule expression retournant une valeur, vous pouvez :
- Supprimer les accolades {}
- Supprimer le mot-clé return
3. L'API Streams : Une Chaîne de Montage pour vos Données
Un Stream n'est pas une collection qui stocke des données. C'est un flux de données provenant d'une source sur lequel on peut appliquer une série d'opérations.
Analogie : Une Chaîne de Montage d'Usine
La Source (.stream()) : C'est le stock de matières premières. L'appel à .stream() place ces matières sur le tapis roulant.
Les Opérations Intermédiaires : Ce sont les stations de travail. Chaque station prend un objet, le transforme ou le rejette, et le passe à la suivante. Ces opérations sont "paresseuses" (lazy).
L'Opération Terminale : C'est la dernière station qui produit le résultat final. C'est celle-ci qui déclenche tout le processus.
Flux de Traitement :
Source
.stream()
→
Filtrer
.filter()
→
Transformer
.map()
→
Résultat
.collect()
Les Opérations Intermédiaires
→ filter(predicate)
La Station de Contrôle Qualité. Filtre les éléments du flux. La lambda doit retourner un boolean (true pour garder, false pour rejeter).
.filter(nom -> nom.length() > 4)
→ map(function)
La Station de Transformation. Transforme chaque élément du flux. La lambda reçoit un élément et en retourne un autre.
.map(String::toUpperCase)
→ sorted()
La Station de Tri. Trie les éléments selon leur ordre naturel.
.sorted()
Les Opérations Terminales
→ collect(collector)
La Station d'Emballage. Rassemble les éléments du flux dans une nouvelle collection.
.collect(Collectors.toList())
→ forEach(consumer)
La Station d'Action Finale. Applique une action à chaque élément, sans rien retourner.
.forEach(System.out::println)
→ count()
La Station de Comptage. Retourne le nombre d'éléments du flux.
long nombre = stream.count();
4. Exemple Pratique Détaillé : Avant et Après
Reprenons un exemple concret. Nous avons une liste de noms et nous voulons obtenir uniquement les noms commençant par "A", en majuscules.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class DemoStreams {
public static void main(String[] args) {
List<String> noms = Arrays.asList("Alice", "Bob", "Anna",
"Charles", "Alexandre");
// --- MANIÈRE IMPÉRATIVE ---
List<String> nomsEnA_imperatif = new ArrayList<>();
for (String nom : noms) {
if (nom.startsWith("A")) {
nomsEnA_imperatif.add(nom.toUpperCase());
}
}
System.out.println("Impératif : " + nomsEnA_imperatif);
// --- MANIÈRE DÉCLARATIVE ---
List<String> nomsEnA_declaratif = noms.stream()
.filter(nom -> nom.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Déclaratif : " + nomsEnA_declaratif);
}
}
Résultat :
Impératif : [ALICE, ANNA, ALEXANDRE]
Déclaratif : [ALICE, ANNA, ALEXANDRE]
Décortiquons le Code Déclaratif
Étape par Étape
- noms.stream() : Mettre les noms sur le tapis roulant
- .filter(nom -> nom.startsWith("A")) : Contrôle qualité - garder seulement ceux commençant par "A"
- .map(String::toUpperCase) : Transformation - convertir chaque nom en majuscules
- .collect(Collectors.toList()) : Emballage - mettre les résultats dans une nouvelle liste
Note sur String::toUpperCase
C'est une "référence de méthode". C'est une manière encore plus courte d'écrire la lambda nom -> nom.toUpperCase(). Le compilateur le comprend comme "pour chaque élément, applique la méthode toUpperCase".