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());
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ération | Signature | Rôle |
|---|---|---|
filter | filter(Predicate<T>) | Garder les éléments qui satisfont la condition |
map | map(Function<T, R>) | Transformer chaque élément |
sorted | sorted(Comparator<T>) | Trier les éléments |
distinct | distinct() | Supprimer les doublons |
limit | limit(long n) | Garder les n premiers éléments |
peek | peek(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ération | Rô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(", "));
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
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));
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.
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().
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.