Introduction aux Streams en Java 8
Les Streams, introduits avec Java 8, offrent une approche déclarative pour le traitement de collections de données. Voici quelques principes fondamentaux à connaître :
- Un Stream ne peut être utilisé qu'une seule fois. Toute tentative de réutilisation entraînera une
IllegalStateException. - Les opérations intermédiaires sur un Stream retournent une nouvelle instance de Stream, permettant le chaînage des opérations.
- Les opérations intermédiaires ne sont exécutées que lorsqu'une opération terminale est appelée. Sans opération terminale, le traitement des données ne démarre pas.
Création de Streams
Il existe plusieurs manières de créer un Stream :
À partir de Collections
La plupart des classes de collections implémentent l'interface Collection, qui fournit la méthode stream().
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.stream.Stream;
// Exemple de création à partir d'une List
List<string> maListe = new ArrayList<>();
Stream<string> streamListe = maListe.stream();
// Exemple de création à partir d'un Set
Set<string> monSet = new HashSet<>();
Stream<string> streamSet = monSet.stream();
// Exemple de création à partir des clés, valeurs ou entrées d'une Map
Map<string string=""> maMap = Map.of("a", "apple", "b", "banana");
Stream<string> streamCle = maMap.keySet().stream();
Stream<string> streamValeur = maMap.values().stream();
Stream<map.entry string="">> streamEntree = maMap.entrySet().stream();
</map.entry></string></string></string></string></string></string></string>
Utilisation de Stream.of()
La classe Stream fournit également des méthodes statiques pour créer des Streams à partir de tableaux ou d'éléments individuels.
import java.util.stream.Stream;
// Création à partir d'éléments individuels
Stream<string> streamElements = Stream.of("un", "deux", "trois");
// Création à partir d'un tableau de chaînes
String[] tabChaines = {"un", "deux", "trois"};
Stream<string> streamTableauChaines = Stream.of(tabChaines);
// Attention : Créer un Stream à partir d'un tableau de type primitif
// traitera le tableau entier comme un seul élément.
int[] tabEntiers = {1, 2, 3};
Stream<int> streamTableauEntiers = Stream.of(tabEntiers); // streamTableauEntiers contient un seul élément : le tableau int[]
</int></string></string>
Opérations Courantes sur les Streams
Les Streams offrent une riche API pour manipuler des séquences de données. Voici quelques opérations fréquemment utilisées :
Opérations Intermédiaires
filter(Predicate): Sélectionne les éléments qui satisfont un prédicat donné.limit(long maxSize): Tronque le Stream à une taille maximale spécifiée.skip(long n): Ignore lesnpremiers éléments du Stream.map(Function): Transforme chaque élément du Stream en une nouvelle forme. Par exemple, convertir des chaînes en entiers.sorted(): Trie les éléments du Stream selon leur ordre naturel ou un comparateur spécifié.distinct(): Supprime les éléments dupliqués du Stream.
Opérations Terminales
forEach(Consumer): Exécute une action pour chaque élément du Stream.count(): Retourne le nombre d'éléments dans le Stream.allMatch(Predicate): Vérifie si tous les éléments satisfont un prédicat.anyMatch(Predicate): Vérifie si au moins un élément satisfait un prédicat.noneMatch(Predicate): Vérifie si aucun élément ne satisfait un prédicat.findFirst(): Retourne unOptionalcontenant le premier élément du Stream, ou unOptionalvide si le Stream est vide.max(Comparator)/min(Comparator): Retourne unOptionalcontenant le plus grand/plus petit élément selon le comparateur fourni.reduce(T identity, BinaryOperator accumulator): Réduit les éléments du Stream en une seule valeur, en utilisant une valeur d'identité et une fonction d'accumulation.
Exemples d'Opérations
import java.util.Optional;
import java.util.stream.Stream;
// Création d'un Stream d'exemple
Stream<string> monStream = Stream.of("pomme", "banane", "cerise", "datte", "banane");
// Filtrage : éléments de longueur 5
monStream.filter(s -> s.length() == 5).forEach(System.out::println); // Affiche "pomme"
// Réinitialisation du stream pour les exemples suivants (un stream ne peut être consommé qu'une fois)
monStream = Stream.of("pomme", "banane", "cerise", "datte", "banane");
// Limiter : les 2 premiers éléments
monStream.limit(2).forEach(System.out::println); // Affiche "pomme", "banane"
monStream = Stream.of("pomme", "banane", "cerise", "datte", "banane");
// Sauter : ignorer les 2 premiers éléments
monStream.skip(2).forEach(System.out::println); // Affiche "cerise", "datte", "banane"
monStream = Stream.of("10", "20", "30");
// Mapper : convertir en entiers
Stream<integer> streamEntiers = monStream.map(Integer::parseInt);
streamEntiers.forEach(System.out::println); // Affiche 10, 20, 30
monStream = Stream.of("10", "20", "30");
// Trier et mapper
Stream<integer> streamTrie = monStream.map(Integer::parseInt).sorted();
streamTrie.forEach(System.out::println); // Affiche 10, 20, 30
monStream = Stream.of("pomme", "banane", "cerise", "datte", "banane");
// Supprimer les doublons
monStream.distinct().forEach(System.out::println); // Affiche "pomme", "banane", "cerise", "datte"
monStream = Stream.of("un", "deux", "trois");
// Correspondances
System.out.println("Tous > 2 : " + monStream.allMatch(s -> s.length() > 2)); // true
monStream = Stream.of("un", "deux", "trois");
System.out.println("Au moins un > 2 : " + monStream.anyMatch(s -> s.length() > 2)); // true
monStream = Stream.of("un", "deux", "trois");
System.out.println("Aucun > 2 : " + monStream.noneMatch(s -> s.length() > 2)); // false
monStream = Stream.of("un", "deux", "trois");
// Trouver le premier
Optional<string> premier = monStream.findFirst();
premier.ifPresent(System.out::println); // Affiche "un"
// Réduire : somme des nombres
Optional<integer> somme = Stream.of(1, 2, 3, 4)
.reduce(Integer::sum); // ou (a, b) -> a + b
somme.ifPresent(val -> System.out.println("Somme : " + val)); // Affiche "Somme : 10"
// Réduire avec une valeur d'identité
int sommeAvecIdentite = Stream.of(1, 2, 3, 4)
.reduce(0, Integer::sum);
System.out.println("Somme avec identité : " + sommeAvecIdentite); // Affiche "Somme avec identité : 10"
// Réduire pour trouver le maximum
Optional<integer> max = Stream.of(5, 1, 9, 3)
.max(Integer::compareTo);
max.ifPresent(val -> System.out.println("Maximum : " + val)); // Affiche "Maximum : 9"
</integer></integer></string></integer></integer></string>
Streams Numériques
Pour améliorer les performances lors du traitement de types primitifs comme int, long ou double, Java propose des Streams spécialisés : IntStream, LongStream, et DoubleStream. Ils évitent l'autoboxing/unboxing coûteux.
import java.util.stream.IntStream;
// Création d'un IntStream
IntStream intervalle = IntStream.rangeClosed(1, 5); // Inclut 1 et 5
// Filtrage et affichage
intervalle.filter(n -> n % 2 == 0).forEach(System.out::println); // Affiche 2, 4
Concaténation de Streams
La méthode Stream.concat() permet de combiner deux Streams en un seul.
import java.util.stream.Stream;
Stream<string> stream1 = Stream.of("alpha", "beta");
Stream<string> stream2 = Stream.of("gamma", "delta");
Stream<string> streamCombine = Stream.concat(stream1, stream2);
streamCombine.forEach(System.out::println); // Affiche "alpha", "beta", "gamma", "delta"
// Attention : Les streams originaux (stream1, stream2) ne doivent plus être utilisés après la concaténation.
</string></string></string>
Streams Parallèles
Les Streams parallèles permettent de tirer parti des processeurs multi-cœurs pour accélérer le traitement des données.
Création de Streams Parallèles
Vous pouvez créer un Stream parallèle de deux manières principales :
- Via la méthode
parallelStream()sur une collection. - En appelant la méthode
parallel()sur un Stream existent.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
List<string> maListe = new ArrayList<>();
// Méthode 1 : direct via la collection
Stream<string> streamParalleleCollection = maListe.parallelStream();
// Méthode 2 : conversion d'un stream séquentiel en parallèle
Stream<string> streamSequentiel = maListe.stream();
Stream<string> streamParalleleConversion = streamSequentiel.parallel();
</string></string></string></string>
Utilisation et Comportement des Streams Parallèles
Lors de l'utilisation de Streams parallèles, les opérations peuvent être exécutées sur différents threads. L'ordre d'exécution des éléments et des étapes intermédiaires n'est pas garanti.
import java.util.stream.Stream;
Stream.of("un", "deux", "trois", "quatre", "cinq")
.parallel() // Convertit en flux parallèle
.filter(s -> {
System.out.println(Thread.currentThread().getName() + " - " + s); // Affiche le thread exécutant l'opération
return s.length() > 3;
})
.count(); // Opération terminale qui déclenche le traitement
// L'ordre d'affichage des éléments par thread variera à chaque exécution.
Considérations sur la Sécurité des Threads
Il est crucial de comprendre que les opérations sur les Streams parallèles ne sont pas intrinsèquement sûres pour les threads si les opérations elles-mêmes ne le sont pas. Par exemple, l'ajout concurrent à une ArrayList non synchronisée depuis plusieurs threads peut entraîner des erreurs.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
// Exemple problématique (ne pas utiliser en production sans synchronisation)
// ArrayList<integer> listeNonSecurisee = new ArrayList<>();
// IntStream.rangeClosed(1, 1000)
// .parallel()
// .forEach(i -> listeNonSecurisee.add(i)); // Risque d'erreur concurrente
// System.out.println("Taille (non sécurisée) : " + listeNonSecurisee.size());
// Solution sécurisée : utiliser une collection concurrente ou synchronisée
List<integer> listeSecurisee = Collections.synchronizedList(new ArrayList<>());
IntStream.rangeClosed(1, 1000)
.parallel()
.forEach(i -> listeSecurisee.add(i));
System.out.println("Taille (sécurisée) : " + listeSecurisee.size()); // Sera 1000
</integer></integer>