L'API Stream en Java : Moderniser le traitement des collections

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

  1. Méthode statique : Classe::methodeStatique ``` // Lambda : s -> Integer.parseInt(s) Function<String, Integer> f = Integer::parseInt;
  2. Constructeur : Classe::new ``` // Lambda : name -> new User(name) Function<String, User> create = User::new;
  3. 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;
  4. 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.

Étiquettes: Java Stream-API Lambda-Expressions functional-programming

Publié le 26 juin à 17h46