Introduction aux flux (Streams) en Java
L'API Stream, introduite avec Java 8, a radicalement changé la manière dont les développeurs manipulent les collections. Elle permet de passer d'une approche impérative (comment faire) à une approche déclarative (quoi faire).
Approche traditionnelle vs Approche Stream
Considérons une liste de noms que nous souhaitons filtrer selon deux critères : commencer par la lettre "M" et posséder exactement 4 caractères.
Méthode classique par itération
List<String> prenoms = Arrays.asList("Marc", "Marie", "Luc", "Jean", "Malo", "Sophie");
// Premier filtrage : commence par 'M'
List<String> filtreM = new ArrayList<>();
for (String nom : prenoms) {
if (nom.startsWith("M")) {
filtreM.add(nom);
}
}
// Deuxième filtrage : longueur de 4
List<String> resultatFinal = new ArrayList<>();
for (String nom : filtreM) {
if (nom.length() == 4) {
resultatFinal.add(nom);
}
}
// Affichage
for (String nom : resultatFinal) {
System.out.println(nom);
}
Cette approche multiplie les structures de contrôle et les collections intermédiaires, ce qui alourdit le code et augmente les risques d'erreurs.
Solutoin avec l'API Stream
prenoms.stream()
.filter(nom -> nom.startsWith("M"))
.filter(nom -> nom.length() == 4)
.forEach(System.out::println);
Philosophie des Streams
Un Stream n'est pas une structure de données qui stocke des éléments ; c'est un conduit qui transporte des données provenant d'une source (collection, tableau, I/O) à travers un pipeline d'opérations.
- Source : La collection ou le tableau d'origine.
- Opérations intermédiaires : Elles transforment un flux en un autre (ex:
filter,map). Elles sont "paresseuses" (lazy) : elles ne s'exécutent que lorsqu'une opération terminale est appelée. - Opération terminale : Elle produit un résultat ou un effet de bord (ex:
collect,forEach,count). Une fois cette étape franchie, le flux est consommé et ne peut plus être réutilisé.
Initialisation d'un Stream
Il existe plusieurs façons de générer un flux de données :
// Depuis une liste
List<Integer> nombres = List.of(10, 20, 30);
Stream<Integer> fluxDepuisListe = nombres.stream();
// Depuis un tableau
String[] fruits = {"Pomme", "Poire", "Banane"};
Stream<String> fluxDepuisTableau = Stream.of(fruits);
// Depuis des valeurs directes
Stream<Double> fluxValeurs = Stream.of(1.5, 2.8, 3.14);
// Cas particulier des Maps
Map<String, Integer> inventaire = new HashMap<>();
Stream<String> fluxClefs = inventaire.keySet().stream();
Stream<Integer> fluxValeursMap = inventaire.values().stream();
Stream<Map.Entry<String, Integer>> fluxEntrees = inventaire.entrySet().stream();
Opérations Intermédiaires Essentielles
Le filtrage avec filter()
La méthode filter utilise un Predicate pour ne conserver que les éléments répondant à une condition booléenne.
Stream.of(10, 15, 20, 25)
.filter(n -> n % 2 == 0) // Garde uniquement les nombres pairs
.forEach(System.out::println);
La transformation avec map()
La méthode map permet de convertir chaque élément du flux vers un autre type ou une autre valeur via une fonction.
List<String> chiffres = List.of("1", "2", "3");
Stream<Integer> fluxEntiers = chiffres.stream()
.map(Integer::parseInt);
Le découpage avec limit() et skip()
limit(n): Récupère uniquement les n premiers éléments.skip(n): Ignore les n premiers éléments et conserve la suite.
Fusion de flux avec concat()
Stream<String> s1 = Stream.of("A", "B");
Stream<String> s2 = Stream.of("C", "D");
Stream<String> fusion = Stream.concat(s1, s2);
Les Références de Méthodes (Method References)
Les références de méthodes (::) sont un raccourci syntaxique pour des expressions Lambda simples.
Types de références
- Méthode statique :
Classe::methodeStatique``` // Lambda : s -> Integer.parseInt(s) Function<String, Integer> f = Integer::parseInt; - Constructeur :
Classe::new``` // Lambda : name -> new User(name) Function<String, User> create = User::new; - Méthode d'instance d'un objet arbitraire :
Classe::methodeInstance``` // Lambda : (s, begin, end) -> s.substring(begin, end) TriFunction<String, Integer, Integer, String> sub = String::substring; - Méthode d'instance d'un objet existant :
objet::methodeInstance``` // Lambda : s -> System.out.println(s) Consumer<String> printer = System.out::println;
Exemple de traitement complexe
Imaginons la gestion de deux listes de participants. Nous voulons fusionner les listes, exclure les noms trop courts, transformer les noms en objets Membre et les afficher.
List<String> groupeA = List.of("Alain", "Bob", "Catherine", "David");
List<String> groupeB = List.of("Eve", "Frédéric", "Guy", "Hélène");
Stream<String> fluxA = groupeA.stream()
.filter(nom -> nom.length() > 3)
.limit(2);
Stream<String> fluxB = groupeB.stream()
.filter(nom -> nom.contains("e"))
.skip(1);
Stream.concat(fluxA, fluxB)
.map(Membre::new)
.forEach(System.out::println);
Note importante : Un Stream ne peut être parcouru qu'une seule fois. Si vous tentez d'appeler une opération sur un flux déjà fermé par une opération treminale, Java lèvera une expection IllegalStateException.