Fonctionnalités de Java 8 : Présentation détaillée des Streams et exemples d'utilisation

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 les n premiers é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 un Optional contenant le premier élément du Stream, ou un Optional vide si le Stream est vide.
  • max(Comparator) / min(Comparator) : Retourne un Optional contenant 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>

Étiquettes: Java 8 Stream java streams Programmation Fonctionnelle Java Concurrency

Publié le 16 juillet à 09h24