Bonjour ! Dans la leçon précédente, nous avons appris à manipuler les collections List, Set et Map avec des boucles et des méthodes traditionnelles. C'est une compétence fondamentale. Cependant, pour des opérations complexes — filtrer une liste, extraire des données spécifiques, puis les regrouper — ce code peut vite devenir verbeux et difficile à lire.
Aujourd'hui, nous allons découvrir une approche plus moderne et puissante, introduite avec Java 8. L'objectif de cette leçon est d'utiliser les streams et les lambdas pour le traitement fonctionnel des collections. Nous allons voir comment écrire des traitements de données qui sont non seulement plus concis, mais aussi beaucoup plus expressifs et lisibles.
Si vous avez déjà utilisé les méthodes filter(), map() ou reduce() sur des tableaux en JavaScript, vous retrouverez ici une philosophie très similaire. Nous allons simplement l'adapter au monde typé et structuré de Java.
1. Le "Pourquoi" : Expressions Lambda et Programmation Fonctionnelle
Avant de manipuler des streams, nous devons comprendre leur principal carburant : les expressions lambda. Une lambda est simplement une manière concise d'écrire une fonction anonyme.
Au lieu d'écrire une méthode complète :
public int somme(int a, int b) {
return a + b;
}
On peut l'exprimer avec une lambda :
(int a, int b) -> a + b
Cette syntaxe nous permet de passer un comportement (une fonction) en paramètre d'une autre méthode, un pilier de la programmation fonctionnelle.
Pour comprendre les bases des lambdas et leur intégration en Java, la ressource suivante est une excellente introduction.
Programmation fonctionnelle et lambdas
Ce document explique ce qu'est la programmation fonctionnelle, la syntaxe des lambdas, et le concept crucial d'interface fonctionnelle qui les rend possibles en Java.
Lisez les sections "Programmation fonctionnelle et lambdas", "Les fonctions anonymes : les lambdas" et "Usage des lambdas". Concentrez-vous sur la syntaxe et comment une lambda peut remplacer une boucle for pour une opération forEach.
Interfaces Fonctionnelles : le mécanisme sous-jacent
Comme vous l'avez lu, Java, étant un langage fortement orienté objet, a une astuce pour traiter les fonctions comme des données : les interfaces fonctionnelles.
Une interface fonctionnelle est une interface qui ne contient qu'une seule méthode abstraite. Runnable ou Comparator sont des exemples que vous connaissez peut-être déjà. Java 8 a introduit l'annotation @FunctionalInterface pour marquer ces interfaces.
Quand vous écrivez une lambda, le compilateur Java la transforme en une implémentation de l'interface fonctionnelle attendue.
// Predicate est une interface fonctionnelle de Java avec la méthode `boolean test(T t)`
// Notre lambda `s -> s.startsWith("A")` est une implémentation de cette méthode.
Predicate<String> startsWithA = s -> s.startsWith("A");
System.out.println(startsWithA.test("Apple")); // true
System.out.println(startsWithA.test("Banana")); // false
Le package java.util.function contient des dizaines d'interfaces fonctionnelles prêtes à l'emploi (Predicate, Function, Consumer, Supplier, etc.), nous évitant de devoir créer les nôtres la plupart du temps.
2. L'API Stream : une chaîne de traitement de données
Maintenant que nous avons les lambdas, nous pouvons les utiliser pour piloter l'API Stream.
Un Stream n'est pas une collection, mais plutôt un flux de données provenant d'une source (comme une List ou un Set) sur lequel on peut appliquer une série d'opérations pour produire un résultat. On peut se le représenter comme une chaîne de montage (pipeline) pour les données.

Un pipeline de stream se compose de trois parties :
- Source : On obtient un stream à partir d'une collection avec la méthode
.stream().maListe.stream()
- Opérations intermédiaires (0 ou plus) : Ce sont des opérations qui transforment le stream en un autre stream. Elles sont paresseuses (lazy), c'est-à-dire qu'elles ne sont exécutées que lorsqu'une opération terminale est appelée. Les plus courantes sont :
filter(): Sélectionne les éléments qui respectent une condition.map(): Transforme chaque élément en autre chose.sorted(): Trie les éléments.distinct(): Supprime les doublons.
- Opération terminale (1 seule) : Déclenche le traitement de toutes les opérations intermédiaires et produit un résultat final. Elle "consomme" le stream, qui ne peut plus être utilisé après.
collect(): Regroupe les résultats dans une collection (List,Set,Map).forEach(): Applique une action à chaque élément.count(): Compte le nombre d'éléments.findFirst(),anyMatch(): Recherche un élément.
3. Les opérations de Stream en action
La meilleure façon de comprendre est de voir le code. La vidéo suivante est un excellent tutoriel pratique qui couvre les opérations les plus importantes.
Java POO : Tutoriel API Stream (Filter / AnyMatch / Collectors)
Cette vidéo de JirAWS vous montre comment utiliser concrètement les opérations de Stream les plus courantes sur une liste d'objets.
Regardez la vidéo du début jusqu'à 07:10. Portez une attention particulière aux points suivants : La comparaison entre une boucle for et un stream (00:53). L'utilisation de anyMatch pour vérifier une condition (01:56). L'enchaînement de filter pour sélectionner des données et de collect pour créer une nouvelle liste (03:18). L'utilisation de map pour transformer des objets (extraire une propriété) (05:07).
Synthétisons avec un exemple concret. Imaginons que nous avons une liste de produits et nous voulons obtenir la liste des noms des produits de la catégorie "Électronique" qui coûtent plus de 500.
// Notre classe simple
class Produit {
String nom;
String categorie;
double prix;
// Constructeur, getters...
}
List<Produit> produits = // ... initialisation de la liste
// --- Approche impérative (avec une boucle) ---
List<String> nomsProduitsChers = new ArrayList<>();
for (Produit p : produits) {
if ("Électronique".equals(p.getCategorie()) && p.getPrix() > 500) {
nomsProduitsChers.add(p.getNom());
}
}
System.out.println(nomsProduitsChers);
// --- Approche déclarative (avec un Stream) ---
List<String> nomsProduitsChersStream = produits.stream() // 1. Obtenir le stream
.filter(p -> "Électronique".equals(p.getCategorie())) // 2. Filtrer par catégorie
.filter(p -> p.getPrix() > 500) // 3. Filtrer par prix
.map(p -> p.getNom()) // 4. Transformer Produit -> String (le nom)
.collect(Collectors.toList()); // 5. Collecter les résultats dans une liste
System.out.println(nomsProduitsChersStream);
Le code du stream se lit presque comme une phrase décrivant le problème. C'est le principal avantage : l'intention est claire.
4. Une opération plus complexe : flatMap
Parfois, nos structures de données sont imbriquées. Par exemple, une liste de commandes, où chaque commande contient une liste de produits. Si nous voulons obtenir une liste unique de tous les produits de toutes les commandes, map ne suffit pas.
Si on fait commandes.stream().map(c -> c.getProduits()), on obtiendra un Stream<List<Produit>>, un "stream de listes". Ce que nous voulons, c'est un Stream<Produit>, un "stream aplati". C'est le rôle de flatMap.
Java POO : Tutoriel API Stream (Filter / AnyMatch / Collectors)
La suite de la vidéo précédente explique flatMap avec un exemple clair de listes imbriquées.
Regardez de 07:10 à 09:05. Concentrez-vous sur la différence entre le résultat de map (une List<List<String>>) et celui de flatMap (une List<String>).
class Commande {
List<Produit> produits;
// ...
}
List<Commande> commandes = // ... liste de commandes
// Obtenir une liste unique de tous les produits de toutes les commandes
List<Produit> tousLesProduits = commandes.stream() // Stream<Commande>
.flatMap(commande -> commande.getProduits().stream()) // Stream<Produit>
.collect(Collectors.toList());
flatMap prend chaque élément (une Commande), le transforme en un Stream (le stream() de sa liste de produits), puis "aplatit" tous ces petits streams en un seul grand stream.
5. Pour aller plus loin : Références de méthodes
Souvent, une lambda ne fait qu'appeler une méthode existante. Par exemple : p -> p.getNom(). Java fournit une syntaxe encore plus courte pour cela : les références de méthodes, avec l'opérateur ::.
// Utilisation d'une lambda
.map(p -> p.getNom())
.forEach(nom -> System.out.println(nom))
// Utilisation d'une référence de méthode
.map(Produit::getNom)
.forEach(System.out::println)
Cette syntaxe est très courante dans le code Java moderne et est considérée comme une bonne pratique car elle est souvent plus lisible.
Programmation fonctionnelle et lambdas
Pour finir, revenons à notre ressource sur les lambdas pour découvrir cette syntaxe concise et très utilisée.
Lisez la section "L’opérateur :: de référence de méthode". Essayez de comprendre les différents types de références (statique, instance, constructeur).
Conclusion
Félicitations ! Vous venez d'acquérir une compétence essentielle pour tout développeur Java moderne. L'API Stream, combinée aux lambdas, est omniprésente dans les frameworks comme Spring Boot et permet d'écrire un code plus propre, plus lisible et plus maintenable.
Points clés à retenir :
- Les expressions lambda (
->) sont des fonctions anonymes qui permettent d'écrire du code concis. - Elles fonctionnent en Java grâce aux interfaces fonctionnelles (interfaces avec une seule méthode abstraite).
- L'API Stream permet de créer un pipeline de traitement pour les collections :
source -> opérations intermédiaires -> opération terminale. - Les opérations intermédiaires (
filter,map,sorted) sont lazy et retournent un nouveau stream. - L'opération terminale (
collect,forEach,count) déclenche le calcul et produit un résultat final. flatMapest utilisé pour "aplatir" des structures de données imbriquées (ex: un stream de listes en un seul stream).- Les références de méthodes (
::) sont une syntaxe encore plus courte pour les lambdas qui ne font qu'appeler une méthode existante.
Dans la prochaine leçon, nous aborderons un autre aspect crucial de la robustesse d'une application : la gestion des exceptions. Nous verrons comment utiliser les blocs try-catch-finally pour gérer les erreurs d'exécution et comment définir nos propres exceptions personnalisées pour un meilleur contrôle des erreurs métier.