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.
GO TO FULL VERSION