CodeGym /Cours /JAVA 25 SELF /Zip (zip), génération de flux (iterate, generate)

Zip (zip), génération de flux (iterate, generate)

JAVA 25 SELF
Niveau 32 , Leçon 3
Disponible

1. Introduction à zip

En programmation, le terme zip (ou « zip », « appariement ») désigne une opération qui prend deux (ou plusieurs) listes et les combine en un seul flux de paires : un élément de chaque liste. Si vous connaissez Python, il y existe une fonction zip qui fait exactement cela.

Exemple :

  • On a une liste de prénoms : ["Alice", "Boris", "Victoria"]
  • On a une liste d’âges : [20, 25, 19]
  • Après le zip, on obtient : [("Alice", 20), ("Boris", 25), ("Victoria", 19)]

C’est pratique lorsqu’il faut traiter deux collections de manière synchrone — par exemple, créer des objets où le prénom et l’âge vont ensemble.

Pourquoi l’API Stream n’a-t-elle pas de zip ?
Dans l’Stream API standard (jusqu’à Java 22), il n’y a pas de méthode zip. La raison est que les streams peuvent être infinis, les collections — de longueurs différentes, et il n’est pas toujours évident de définir le comportement quand l’une est plus longue que l’autre. Mais en pratique, zip est souvent nécessaire.

2. Implémenter zip en Java : comment faire sans méthode intégrée

La méthode la plus simple — via les index

Si vous avez deux listes et que vous savez qu’il s’agit de collections « classiques » (par exemple, List<A>, List<B>), vous pouvez utiliser les index :

import java.util.*;
import java.util.stream.*;

public class ZipExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Boris", "Victoria");
        List<Integer> ages = Arrays.asList(20, 25, 19);

        int size = Math.min(names.size(), ages.size());

        List<Person> people = IntStream.range(0, size)
            .mapToObj(i -> new Person(names.get(i), ages.get(i)))
            .collect(Collectors.toList());

        people.forEach(System.out::println);
    }

    static class Person {
        String name;
        int age;
        Person(String name, int age) { this.name = name; this.age = age; }
        public String toString() { return name + " (" + age + ")"; }
    }
}

Sortie :

Alice (20)
Boris (25)
Victoria (19)

Que se passe-t-il ?

  • On prend la taille minimale des deux listes — pour ne pas dépasser les bornes.
  • On utilise IntStream.range(0, size) — on crée un flux d’index.
  • Pour chaque index, on prend un élément de chaque liste et on les apparie.
  • On collecte le résultat dans une liste via Collectors.toList().

Peut-on réaliser un zip pour Stream<T> ?

Techniquement — oui, mais c’est pratique seulement lorsque les deux streams sont finis et adossés à une structure à accès indexé rapide (autrement dit, ce sont en fait des List). Pour de « vrais » flux (par exemple infinis), une implémentation correcte de zip est plus complexe et demande une logique supplémentaire.

Alternatives : bibliothèques tierces

Si vous voulez un zip « prêt à l’emploi », vous pouvez utiliser des bibliothèques :

  • org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
  • io.vavr.collection.Stream.zip (Vavr)
  • com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)

Dans ce cours, nous restons sur la bibliothèque standard ; nous examinons donc des méthodes « manuelles ».

3. Exemples pratiques d’utilisation de zip

Exemple 1. Parcours synchronisé de deux collections (somme des éléments)

List<Integer> a = Arrays.asList(1, 2, 3, 4);
List<Integer> b = Arrays.asList(10, 20, 30, 40);

List<Integer> sums = IntStream.range(0, Math.min(a.size(), b.size()))
    .mapToObj(i -> a.get(i) + b.get(i))
    .collect(Collectors.toList());

System.out.println(sums); // [11, 22, 33, 44]

Exemple 2. Zip de chaînes et de caractères

String[] words = {"cat", "dog", "fox"};
char[] marks = {'!', '?', '.'};

List<String> zipped = IntStream.range(0, Math.min(words.length, marks.length))
    .mapToObj(i -> words[i] + marks[i])
    .collect(Collectors.toList());

System.out.println(zipped); // [cat!, dog?, fox.]

Visualisation (schéma)

noms:    [Alice] [Boris] [Victoria]
âges:    [20 ] [25   ] [19  ]
          |      |      |
zip ---> (Alice,20) (Boris,25) (Victoria,19)

4. Stream.iterate et Stream.generate — génération de nouveaux flux

Parfois, nous devons non seulement traiter des collections existantes, mais aussi créer de nouvelles séquences « à la volée ». Pour cela, l’Stream API fournit deux méthodes utiles :

  • Stream.iterate — crée une séquence selon une règle (par exemple, une progression arithmétique).
  • Stream.generate — crée un flux où chaque élément est calculé par un Supplier (par exemple, un nombre aléatoire, l’heure courante, etc.).

Stream.iterate

Syntaxe :

Stream.iterate(seed, unaryOperator)
  • seed — valeur initiale ;
  • unaryOperator — fonction qui calcule l’élément suivant.

Exemple 1 : progression arithmétique

Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// Affiche : 0 2 4 6 8

Exemple 2 : Génération de dates

import java.time.LocalDate;

Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// Par exemple : 2024-06-09, 2024-06-10, 2024-06-11

Exemple 3 : flux infini — n’oubliez pas limit !

Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16

Depuis Java 9+ une variante surchargée avec un prédicat de condition est apparue :

Stream.iterate(0, n -> n < 10, n -> n + 2)
    .forEach(System.out::println); // 0 2 4 6 8

Stream.generate

Syntaxe :

Stream.generate(Supplier<T>)

Chaque élément est calculé par un appel à Supplier.get().

Exemple 1 : Nombres aléatoires

import java.util.Random;

Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);

Exemple 2 : Génération de valeurs identiques

Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****

Exemple 3 : Identifiants uniques

import java.util.UUID;

Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);

Visualisation (schéma)

Stream.iterate :

[seed] -> op() -> op() -> op() -> ...
       n      n+1    n+2    n+3

Stream.generate :

Supplier() -> Supplier() -> Supplier() -> ...
    val1         val2          val3

5. Exemples d’utilisation : génération de données pour une application

Supposons que nous ayons une classe Student :

class Student {
    String name;
    int age;
    Student(String name, int age) { this.name = name; this.age = age; }
    public String toString() { return name + " (" + age + ")"; }
}

Exemple 1. Génération d’étudiants de test

List<String> names = Arrays.asList("Alice", "Boris", "Victoria", "George", "Diana");
Stream<Student> students = IntStream.range(0, names.size())
    .mapToObj(i -> new Student(names.get(i), 18 + i));

students.forEach(System.out::println);
// Alice (18), Boris (19), Victoria (20), George (21), Diana (22)

Exemple 2. Génération d’étudiants aléatoires

Random random = new Random();
List<String> pool = Arrays.asList("Irene", "Oliver", "Max", "Tara", "Steven");

Stream<Student> randomStudents = Stream.generate(() ->
    new Student(
        pool.get(random.nextInt(pool.size())),
        18 + random.nextInt(5)
    )
);

randomStudents.limit(3).forEach(System.out::println);
// Par exemple : Tara (19), Oliver (21), Irene (20)

Exemple 3. Génération d’une séquence de dates pour un rapport

import java.time.LocalDate;

Stream<LocalDate> dates = Stream.iterate(LocalDate.of(2024, 6, 1), d -> d.plusDays(1));
dates.limit(5).forEach(System.out::println);
// 2024-06-01, 2024-06-02, ..., 2024-06-05

Comparaison : quand utiliser zip, iterate, generate

  • zip — à utiliser quand il faut traiter de façon synchronisée deux (ou plusieurs) listes/flux en associant les éléments par index.
  • iterate — quand il faut une séquence définie par une règle (nombres, dates, étapes).
  • generate — quand chaque élément est calculé indépendamment (valeurs aléatoires, ID uniques).

7. Erreurs courantes lors de l’utilisation de zip et de la génération de flux

Erreur n° 1 : flux non borné sans limit. Si vous utilisez Stream.iterate ou Stream.generate sans les borner via limit, le programme peut se bloquer ou « avaler » toute la mémoire.

Stream.generate(() -> 1).forEach(System.out::println); // Ne se terminera jamais !

Erreur n° 2 : mauvaise gestion des longueurs différentes lors d’un zip. Si une liste est plus longue que l’autre, il faut s’arrêter à la longueur minimale, sinon vous aurez une IndexOutOfBoundsException.

Erreur n° 3 : tenter de zipper des Stream<T> ordinaires. Les streams ordinaires n’ont pas d’accès par index. En pratique, on fait plus souvent un zip pour des List.

Erreur n° 4 : modification d’une collection pendant la génération d’un flux. Si vous modifiez une collection pendant qu’un stream la parcourt, vous pouvez obtenir une ConcurrentModificationException. Générez de nouvelles données — ne modifiez pas les anciennes « à la volée ».

Erreur n° 5 : perte de l’ordre. Si l’ordre compte (par exemple avec zip), utilisez une List, pas un Set — sinon l’ordre des éléments sera imprévisible.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION