Aller au contenu principal

L'API Stream

L'API Stream (Java 8+) permet de traiter des collections comme un pipeline de données : chaque opération transforme ou filtre les éléments sans jamais modifier la collection d'origine.

Notions théoriques

Qu'est-ce qu'un Stream ?

Un Stream est une séquence d'éléments qui supporte des opérations enchaînées. Il ne stocke pas de données — il les traverse.

List<Personnage> vivants = personnages.stream()
.filter(p -> p.estVivant())
.collect(Collectors.toList());
info

Un Stream ne modifie jamais la collection source. Il produit un nouveau résultat.

On obtient un Stream depuis une collection avec .stream(), ou un Stream parallèle avec .parallelStream().

Opérations intermédiaires (lazy)

Les opérations intermédiaires ne s'exécutent pas immédiatement — elles sont lazy : elles attendent qu'une opération terminale déclenche le pipeline.

OpérationSignatureRôle
filterfilter(Predicate<T>)Garder les éléments qui satisfont la condition
mapmap(Function<T, R>)Transformer chaque élément
sortedsorted(Comparator<T>)Trier les éléments
distinctdistinct()Supprimer les doublons
limitlimit(long n)Garder les n premiers éléments
peekpeek(Consumer<T>)Inspecter sans modifier (débogage)
personnages.stream()
.filter(p -> p.getForce() > 50) // filtre
.map(p -> p.getNom()) // transformation
.sorted() // tri alphabétique
.distinct() // doublons supprimés
.limit(5) // 5 premiers
.peek(nom -> System.out.println("Vu : " + nom)) // inspection
.collect(Collectors.toList());

Opérations terminales

Une opération terminale déclenche le pipeline et produit un résultat. Il ne peut y en avoir qu'une seule par Stream.

OpérationRôle
collect(Collectors.toList())Rassembler dans une List
forEach(Consumer)Parcourir chaque élément
count()Compter les éléments
findFirst()Retourner le premier → Optional<T>
reduce(identité, BinaryOperator)Agréger en une seule valeur
// Compter les guerriers
long nbGuerriers = personnages.stream()
.filter(p -> p instanceof Guerrier)
.count();

// Trouver le premier vivant
Optional<Personnage> premier = personnages.stream()
.filter(Personnage::estVivant)
.findFirst();

// Somme des forces
int totalForce = personnages.stream()
.mapToInt(Personnage::getForce)
.sum();

Collectors avancés

Collectors.groupingBy() regroupe les éléments dans une Map :

// Grouper par type de personnage
Map<String, List<Personnage>> parType = personnages.stream()
.collect(Collectors.groupingBy(p -> p.getClass().getSimpleName()));

// Compter par type
Map<String, Long> compteParType = personnages.stream()
.collect(Collectors.groupingBy(
p -> p.getClass().getSimpleName(),
Collectors.counting()
));

// Joindre les noms en une String
String tousLesNoms = personnages.stream()
.map(Personnage::getNom)
.collect(Collectors.joining(", "));
attention

parallelStream() divise le travail sur plusieurs threads. Ne l'utilisez que sur de grandes collections (>10 000 éléments) et uniquement avec des opérations sans effets de bord.

Exemple pratique

import java.util.*;
import java.util.stream.*;

public class AnalysePersonnages {

public static void main(String[] args) {
List<Personnage> equipe = new ArrayList<>();
equipe.add(new Guerrier("Aragorn", 150, 30));
equipe.add(new Mage("Gandalf", 100, 20, 50));
equipe.add(new Guerrier("Boromir", 130, 28));
equipe.add(new Mage("Saruman", 90, 25, 70));
equipe.add(new Guerrier("Gimli", 140, 35));

// Guerriers triés par force décroissante
List<Personnage> guerriersParForce = equipe.stream()
.filter(p -> p instanceof Guerrier)
.sorted(Comparator.comparingInt(Personnage::getForce).reversed())
.collect(Collectors.toList());

System.out.println("=== Guerriers par force ===");
guerriersParForce.forEach(p ->
System.out.println(p.getNom() + " : " + p.getForce()));

// Nom du personnage le plus fort
Optional<Personnage> lePlusFort = equipe.stream()
.max(Comparator.comparingInt(Personnage::getForce));

lePlusFort.ifPresent(p ->
System.out.println("\nLe plus fort : " + p.getNom()));

// Grouper par type
Map<String, List<Personnage>> parType = equipe.stream()
.collect(Collectors.groupingBy(
p -> p.getClass().getSimpleName()));

parType.forEach((type, liste) -> {
String noms = liste.stream()
.map(Personnage::getNom)
.collect(Collectors.joining(", "));
System.out.println(type + " : " + noms);
});

// Force totale de l'équipe
int forceTotal = equipe.stream()
.mapToInt(Personnage::getForce)
.sum();
System.out.println("\nForce totale de l'équipe : " + forceTotal);
}
}

Test de mémorisation/compréhension


Quelle méthode permet d'obtenir un Stream depuis une List ?


Les opérations intermédiaires d'un Stream sont dites 'lazy'. Que signifie ce terme ?


Quelle opération terminale rassemble les éléments d'un Stream dans une List ?


Que retourne findFirst() si aucun élément ne satisfait le filtre précédent ?


Quel Collector regroupe les éléments dans une Map selon un critère ?


Quelle opération intermédiaire transforme chaque élément en un autre type ?


TP pour réfléchir et résoudre des problèmes

Vous allez analyser une liste de personnages RPG avec l'API Stream.

Étape 1 — Filtrer les personnages vivants et les trier par force

À partir d'une liste de personnages, extrayez les vivants et triez-les par force décroissante.

List<Personnage> equipe = new ArrayList<>();
equipe.add(new Guerrier("Aragorn", 150, 30));
equipe.add(new Guerrier("Boromir", 0, 28)); // mort
equipe.add(new Mage("Gandalf", 100, 20, 50));
equipe.add(new Mage("Saruman", 90, 25, 70));

Bonne pratique - Chaîner les opérations sans variable intermédiaire

L'API Stream est conçue pour enchaîner les opérations. Résistez à la tentation de créer une List intermédiaire entre chaque étape : filter().sorted().collect() en une seule chaîne est plus lisible et plus efficace.

Étape 2 — Trouver le personnage le plus fort

Utilisez max() pour trouver le personnage ayant la plus grande force.


Bonne pratique - Toujours utiliser ifPresent ou orElse avec Optional

max() retourne un Optional qui peut être vide si la liste est vide. N'appelez jamais .get() directement — utilisez .ifPresent(), .orElse() ou .orElseThrow() pour gérer proprement le cas vide.

Étape 3 — Grouper les personnages par type

Créez une Map qui regroupe les personnages par leur type (Guerrier, Mage, etc.) avec Collectors.groupingBy().


Bonne pratique - groupingBy + counting pour des statistiques rapides

Collectors.groupingBy(fn, Collectors.counting()) permet de compter directement sans passer par List. C'est le pattern le plus efficace pour des statistiques groupées.

📌 Une solution