Table of Contents
Introduction aux Streams Java
Les Streams, introduits dans Java 8, representent une revolution dans la maniere de traiter les collections de donnees. Ils permettent d’adopter un style de programmation declaratif, ou vous decrivez ce que vous voulez obtenir plutot que comment l’obtenir.
Le Paradigme Fonctionnel en Java
Avant Java 8, le traitement des collections se faisait de maniere imperative avec des boucles for ou while. Les Streams apportent une approche fonctionnelle inspiree de langages comme Scala ou Haskell :
// Approche imperative (avant Java 8)
List<String> filteredNames = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
filteredNames.add(name.toUpperCase());
}
}
// Approche declarative avec Streams (Java 8+)
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
Caracteristiques Cles des Streams
Les Streams possedent plusieurs caracteristiques fondamentales :
- Non-stockage : Un Stream ne stocke pas les elements, il les transporte depuis une source
- Fonctionnel : Les operations sur un Stream produisent un resultat sans modifier la source
- Lazy evaluation : Les operations intermediaires sont evaluees paresseusement
- Potentiellement infini : Un Stream peut representer une sequence infinie d’elements
- Consommable une seule fois : Un Stream ne peut etre parcouru qu’une seule fois
Les Differents Types de Streams
Java propose plusieurs types de Streams adaptes a differents cas d’usage.
Stream Generique
Le Stream<T> est le type le plus courant, capable de manipuler n’importe quel type d’objet :
Stream<String> stringStream = Stream.of("Java", "Streams", "Tutorial");
Stream<Person> personStream = people.stream();
Streams Primitifs Specialises
Pour eviter le boxing/unboxing couteux avec les types primitifs, Java fournit des streams specialises :
// IntStream pour les entiers
IntStream intStream = IntStream.range(1, 100);
int sum = intStream.sum();
// LongStream pour les longs
LongStream longStream = LongStream.rangeClosed(1, 1_000_000);
long count = longStream.count();
// DoubleStream pour les doubles
DoubleStream doubleStream = DoubleStream.of(1.5, 2.5, 3.5);
double average = doubleStream.average().orElse(0.0);
Conversion Entre Types de Streams
// Stream vers IntStream
IntStream intStream = Stream.of("1", "2", "3")
.mapToInt(Integer::parseInt);
// IntStream vers Stream
Stream<Integer> boxedStream = IntStream.range(1, 10).boxed();
// IntStream vers DoubleStream
DoubleStream doubleStream = IntStream.range(1, 10).asDoubleStream();
Operations Intermediaires vs Terminales
Comprendre la difference entre ces deux types d’operations est crucial pour maitriser les Streams.
Tableau Comparatif
| Aspect | Operations Intermediaires | Operations Terminales |
|---|---|---|
| Retour | Retournent un Stream | Retournent un resultat ou void |
| Evaluation | Lazy (paresseuse) | Declenchent l’execution |
| Chainabilite | Peuvent etre chainees | Terminent le pipeline |
| Exemples | filter, map, sorted, distinct | collect, forEach, reduce, count |
Operations Intermediaires Detaillees
Stream<T> filter(Predicate<T> predicate) // Filtre les elements
Stream<R> map(Function<T, R> mapper) // Transforme les elements
Stream<T> sorted() // Trie les elements
Stream<T> distinct() // Supprime les doublons
Stream<T> limit(long n) // Limite a n elements
Stream<T> skip(long n) // Ignore les n premiers
Stream<T> peek(Consumer<T> action) // Debug sans modifier
Operations Terminales Detaillees
void forEach(Consumer<T> action) // Execute une action
long count() // Compte les elements
Optional<T> findFirst() // Premier element
Optional<T> findAny() // Un element quelconque
boolean anyMatch(Predicate<T> p) // Au moins un match
boolean allMatch(Predicate<T> p) // Tous matchent
boolean noneMatch(Predicate<T> p) // Aucun ne matche
T reduce(T identity, BinaryOperator<T> op) // Reduction
R collect(Collector<T,A,R> collector) // Collection des resultats
Combiner les Resultats avec reduce
La methode reduce() est fondamentale pour agreger les elements d’un Stream en un seul resultat.
Syntaxes de reduce
// Avec identite et accumulateur
T reduce(T identity, BinaryOperator<T> accumulator)
// Sans identite (retourne Optional)
Optional<T> reduce(BinaryOperator<T> accumulator)
// Avec combiner pour parallelisme
U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)
Exemples Pratiques de reduce
// Somme des nombres
int sum = IntStream.range(1, 100).reduce(0, Integer::sum);
// Concatenation de chaines
String concat = Stream.of("A", "B", "C")
.reduce("", String::concat);
// Trouver le maximum
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
// Calcul complexe : moyenne ponderee
double weightedAvg = items.stream()
.reduce(0.0,
(acc, item) -> acc + item.getValue() * item.getWeight(),
Double::sum) / totalWeight;
Methode utilitaire entryMapper
Pour conserver les valeurs initiales lors d’un mapping, utilisez cette methode utilitaire :
public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper) {
return (k) -> new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}
// Utilisation
Set<K> mySet = Set.of(key1, key2, key3);
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
.map(entryMapper(transformer));
Conversion et Manipulation de Streams Specialises
IntStream vers String
La conversion entre IntStream et String est courante lors du traitement de texte :
public IntStream stringToIntStream(String in) {
return in.codePoints();
}
public String intStreamToString(IntStream intStream) {
return intStream.collect(
StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append
).toString();
}
// Exemple : convertir en majuscules via code points
String result = "hello".codePoints()
.map(Character::toUpperCase)
.collect(StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append)
.toString();
// result = "HELLO"
Trouver des Elements avec findFirst et findAny
// Premier element satisfaisant un predicat
OptionalInt firstSquareOver50000 = IntStream.iterate(1, i -> i + 1)
.filter(i -> (i * i) > 50000)
.findFirst();
System.out.println(firstSquareOver50000.getAsInt()); // 224
// findAny est plus performant en parallele
Optional<Person> anyAdult = people.parallelStream()
.filter(p -> p.getAge() >= 18)
.findAny();
References de Methodes et Chaines Complexes
Les references de methodes rendent le code plus lisible et concis.
Types de References de Methodes
// Reference a une methode statique
Function<String, Integer> parser = Integer::parseInt;
// Reference a une methode d'instance sur un objet
String prefix = "Hello";
Predicate<String> startsWith = prefix::startsWith;
// Reference a une methode d'instance sur le type
Function<String, String> toUpper = String::toUpperCase;
// Reference a un constructeur
Supplier<ArrayList<String>> listFactory = ArrayList::new;
Exemple de Pipeline Complexe
public <V extends Ordered> List<V> processThings(List<Thing<V>> things) {
return things.stream()
.filter(Thing::hasPropertyOne) // Filtre sur propriete 1
.map(Thing::getValuedProperty) // Extrait la propriete valorisee
.filter(Objects::nonNull) // Supprime les nulls
.filter(Valued::hasPropertyTwo) // Filtre sur propriete 2
.map(Valued::getValue) // Extrait la valeur
.filter(Objects::nonNull) // Supprime les nulls
.sorted(Comparator.comparing(Ordered::getOrder)) // Trie par ordre
.collect(Collectors.toList()); // Collecte en liste
}
Traitement des Optional dans les Streams
Filtrer les Optional Presents
// Methode classique (Java 8)
List<String> values = Stream.of(opt1, opt2, opt3)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
// Methode moderne (Java 9+) avec flatMap
List<String> values = Stream.of(opt1, opt2, opt3)
.flatMap(Optional::stream)
.collect(Collectors.toList());
Gestion des Valeurs Nulles
// Creer un Stream a partir d'une valeur potentiellement nulle
Stream<String> safeStream = Stream.ofNullable(possibleNullValue);
// Filtrer les nulls dans un Stream
List<String> nonNullValues = mixedList.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
Pagination et Extraction avec skip et limit
Extraire une Tranche de Donnees
final long offset = 20L; // Elements a ignorer
final long pageSize = 10L; // Taille de la page
Stream<T> page = collection.stream()
.skip(offset)
.limit(pageSize);
// Implementation de pagination
public <T> List<T> getPage(List<T> data, int pageNumber, int pageSize) {
return data.stream()
.skip((long) pageNumber * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
}
Combiner avec sorted pour la Pagination Triee
List<Product> topExpensiveProducts = products.stream()
.sorted(Comparator.comparing(Product::getPrice).reversed())
.skip(0)
.limit(10)
.collect(Collectors.toList());
flatMap et Collections Imbriquees
La methode flatMap est essentielle pour aplatir des structures imbriquees.
Concept de flatMap
// map : transforme chaque element en un autre element
// flatMap : transforme chaque element en un Stream, puis aplatit
// Exemple : extraire tous les mots de plusieurs phrases
List<String> sentences = Arrays.asList(
"Java Streams are powerful",
"flatMap is useful",
"Learn functional programming"
);
List<String> words = sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.collect(Collectors.toList());
// ["Java", "Streams", "are", "powerful", "flatMap", "is", "useful", ...]
Aplatir des Collections Imbriquees
// Liste de listes
List<List<Integer>> nestedLists = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5),
Arrays.asList(6, 7, 8, 9)
);
List<Integer> flatList = nestedLists.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Objets avec collections internes
List<String> allSkills = employees.stream()
.flatMap(emp -> emp.getSkills().stream())
.distinct()
.sorted()
.collect(Collectors.toList());
flatMapToInt, flatMapToLong, flatMapToDouble
int totalItems = orders.stream()
.flatMapToInt(order -> order.getItems().stream().mapToInt(Item::getQuantity))
.sum();
Parallelisme avec parallel
Creer un Stream Parallele
// A partir d'une collection
Stream<T> parallelStream = collection.parallelStream();
// Convertir un stream sequentiel
Stream<T> parallel = stream.parallel();
// Revenir en sequentiel
Stream<T> sequential = parallelStream.sequential();
Quand Utiliser parallel
Les streams paralleles sont benefiques quand :
- La source de donnees est volumineuse (> 10000 elements)
- Les operations sont CPU-intensive
- La source supporte bien le splitting (ArrayList, arrays)
// Bon cas : calcul intensif sur grande collection
double average = largeDataset.parallelStream()
.mapToDouble(this::expensiveCalculation)
.average()
.orElse(0.0);
Pieges du Parallelisme
// MAUVAIS : etat mutable partage
List<Integer> results = new ArrayList<>(); // Non thread-safe !
numbers.parallelStream()
.filter(n -> n > 10)
.forEach(results::add); // Race condition !
// BON : utiliser collect
List<Integer> results = numbers.parallelStream()
.filter(n -> n > 10)
.collect(Collectors.toList());
Collectors Avances
Creer un Map avec toMap
Stream<String> characters = Stream.of("A", "B", "C");
Map<Integer, String> map = characters
.collect(Collectors.toMap(
String::hashCode, // Key mapper
Function.identity() // Value mapper
));
// Gerer les doublons
Map<String, Person> personByName = people.stream()
.collect(Collectors.toMap(
Person::getName,
Function.identity(),
(existing, replacement) -> existing // Merge function
));
Groupement avec groupingBy
List<Person> people = Arrays.asList(
new Person("Sam", "Rossi", 25),
new Person("Sam", "Verdi", 30),
new Person("John", "Bianchi", 25),
new Person("John", "Rossi", 35),
new Person("John", "Verdi", 40)
);
// Grouper par prenom
Map<String, List<Person>> byName = people.stream()
.collect(Collectors.groupingBy(Person::getName));
// Grouper avec comptage
Map<String, Long> countByName = people.stream()
.collect(Collectors.groupingBy(
Person::getName,
Collectors.counting()
));
// Groupement multi-niveau
Map<String, Map<Integer, List<Person>>> byNameAndAge = people.stream()
.collect(Collectors.groupingBy(
Person::getName,
Collectors.groupingBy(Person::getAge)
));
Partitionnement avec partitioningBy
// Separer en deux groupes selon un predicat
Map<Boolean, List<Person>> partitioned = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() >= 30));
List<Person> seniors = partitioned.get(true);
List<Person> juniors = partitioned.get(false);
Statistiques avec summarizingInt
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::getAge));
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());
Concatenation avec joining
// Simple concatenation
String names = people.stream()
.map(Person::getName)
.collect(Collectors.joining()); // "SamSamJohnJohnJohn"
// Avec delimiteur
String csv = people.stream()
.map(Person::getName)
.collect(Collectors.joining(", ")); // "Sam, Sam, John, John, John"
// Avec prefix et suffix
String json = people.stream()
.map(Person::getName)
.map(name -> "\"" + name + "\"")
.collect(Collectors.joining(", ", "[", "]")); // ["Sam", "Sam", "John"]
Collecteurs Composes
// Collecter les noms des personnes par ville
Map<String, String> namesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.mapping(
Person::getName,
Collectors.joining(", ")
)
));
// Trouver le plus age par departement
Map<String, Optional<Person>> oldestByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparing(Employee::getAge))
));
Bonnes Pratiques pour les Streams
1. Privilegier les References de Methodes
// Moins lisible
list.stream().map(s -> s.toUpperCase())
// Plus lisible
list.stream().map(String::toUpperCase)
2. Eviter les Streams pour les Operations Simples
// Inutilement complexe
Optional<String> first = list.stream().findFirst();
// Plus simple
String first = list.isEmpty() ? null : list.get(0);
3. Utiliser les Streams Primitifs pour les Calculs Numeriques
// Evite le boxing/unboxing
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
4. Fermer les Streams de Ressources I/O
// Utiliser try-with-resources pour les streams I/O
try (Stream<String> lines = Files.lines(path)) {
lines.filter(line -> line.contains("error"))
.forEach(System.out::println);
}
5. Limiter les Streams Infinis
// TOUJOURS limiter un stream infini
Stream.iterate(0, i -> i + 1)
.limit(100) // Obligatoire !
.forEach(System.out::println);
6. Preferer collect a forEach pour les Resultats
// Anti-pattern
List<String> results = new ArrayList<>();
stream.forEach(results::add);
// Meilleure pratique
List<String> results = stream.collect(Collectors.toList());
Pieges Courants a Eviter
1. Les Effets de Bord dans les Streams
// MAUVAIS : effet de bord dans map
List<String> processed = new ArrayList<>();
items.stream()
.map(item -> {
processed.add(item); // Effet de bord !
return item.transform();
})
.collect(Collectors.toList());
// BON : operations pures
List<String> transformed = items.stream()
.map(Item::transform)
.collect(Collectors.toList());
2. Reutiliser un Stream Ferme
// ERREUR : IllegalStateException
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.count(); // Exception ! Stream deja consomme
// SOLUTION : creer un nouveau stream
list.stream().forEach(System.out::println);
long count = list.stream().count();
3. Ordre des Operations
// Inefficace : filter apres limit
Stream.iterate(1, i -> i + 1)
.limit(1000000)
.filter(i -> i % 2 == 0) // Filtre 500000 elements
.limit(10)
.collect(Collectors.toList());
// Efficace : filter avant limit
Stream.iterate(1, i -> i + 1)
.filter(i -> i % 2 == 0) // Genere seulement les pairs
.limit(10) // S'arrete apres 10
.collect(Collectors.toList());
4. NullPointerException Silencieux
// Risque de NPE
list.stream()
.map(obj -> obj.getProperty()) // NPE si obj est null
.collect(Collectors.toList());
// Securise
list.stream()
.filter(Objects::nonNull)
.map(obj -> obj.getProperty())
.filter(Objects::nonNull)
.collect(Collectors.toList());
5. Performance avec sorted sur Streams Paralleles
// sorted() peut degrader les performances en parallele
// car il necessite une synchronisation
largeList.parallelStream()
.sorted() // Force la synchronisation
.collect(Collectors.toList());
Conclusion
Les Streams Java representent un paradigme puissant pour le traitement des donnees. Ce guide a couvert les aspects essentiels, des operations de base aux patterns avances.
Tableau Recapitulatif des Operations
| Operation | Type | Description | Exemple |
|---|---|---|---|
| filter | Intermediaire | Filtre les elements | .filter(x -> x > 0) |
| map | Intermediaire | Transforme les elements | .map(String::toUpperCase) |
| flatMap | Intermediaire | Aplatit les collections | .flatMap(Collection::stream) |
| sorted | Intermediaire | Trie les elements | .sorted(Comparator.naturalOrder()) |
| distinct | Intermediaire | Supprime les doublons | .distinct() |
| limit | Intermediaire | Limite le nombre | .limit(10) |
| skip | Intermediaire | Ignore les premiers | .skip(5) |
| collect | Terminale | Collecte les resultats | .collect(Collectors.toList()) |
| reduce | Terminale | Agregation | .reduce(0, Integer::sum) |
| forEach | Terminale | Execute une action | .forEach(System.out::println) |
| count | Terminale | Compte les elements | .count() |
| findFirst | Terminale | Premier element | .findFirst() |
| anyMatch | Terminale | Test d’existence | .anyMatch(x -> x > 0) |
Points Cles a Retenir
- Lazy evaluation : Les operations intermediaires ne s’executent qu’au declenchement d’une operation terminale
- Immutabilite : Les Streams ne modifient pas la source de donnees
- Parallelisme : Utilisez
parallel()avec precaution et uniquement pour des operations CPU-intensive sur de grandes collections - Collectors : Maitrisez
groupingBy,partitioningByettoMappour des aggregations complexes - flatMap : Indispensable pour traiter les structures imbriquees
Avec ces connaissances, vous etes maintenant equipe pour exploiter pleinement la puissance des Streams dans vos projets Java.
In-Article Ad
Dev Mode
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Lambda Expressions en Java : Guide Complet pour Simplifier votre Code
Decouvrez comment simplifier votre code Java avec les expressions lambda. Apprenez a remplacer le code boilerplate, maitriser les interfaces fonctionnelles et ecrire du code plus elegant avec Java 8+.
Guide de Programmation Java : Regles d'Indentation, Litteraux et Conventions de Code
Maitrisez les conventions de codage Java : indentation, litteraux, lambdas et fichiers sources. Guide complet pour un code propre et maintenable.
Manipuler les classes Java avec ASM et Javassist : bytecode, instrumentation et fichiers JAR
Apprenez a manipuler les classes Java avec ASM et Javassist : chargement, modification du bytecode, instrumentation et creation de fichiers JAR.