1. Einführung in zip
In der Programmierung bezeichnet der Begriff zip (auch „Zipping“, „Zusammenführen“) eine Operation, die zwei (oder mehr) Listen nimmt und sie zu einem Stream von Paaren kombiniert: jeweils ein Element aus jeder Liste. Wenn Sie mit Python vertraut sind: Dort gibt es die Funktion zip, die genau das tut.
Beispiel:
- Es gibt eine Namensliste: ["Alice", "Bob", "Charlie"]
- Es gibt eine Altersliste: [20, 25, 19]
- Nach dem „Zipping“ ergibt sich: [("Alice", 20), ("Bob", 25), ("Charlie", 19)]
Das ist praktisch, wenn zwei Collections synchron verarbeitet werden sollen — etwa um Objekte zu erzeugen, in denen Name und Alter zusammengehören.
Warum gibt es im Stream API kein zip?
Im standardmäßigen Stream API (bis Java 22) gibt es keine Methode zip. Der Grund: Streams können unendlich sein, Collections — unterschiedlich lang, und es ist nicht immer klar, wie man sich verhalten soll, wenn eine Collection länger ist als die andere. In der Praxis wird zip jedoch oft gebraucht.
2. Zip in Java implementieren: wie ohne eingebaute Methode leben
Der einfachste Weg — über Indizes
Wenn Sie zwei Listen haben und genau wissen, dass es „gewöhnliche“ Collections sind (zum Beispiel List<A>, List<B>), können Sie Indizes verwenden:
import java.util.*;
import java.util.stream.*;
public class ZipExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
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 + ")"; }
}
}
Ausgabe:
Alice (20)
Bob (25)
Charlie (19)
Was passiert hier?
- Wir nehmen die minimale Größe der beiden Listen — um keinen Indexfehler zu bekommen.
- Wir verwenden IntStream.range(0, size) — wir erzeugen einen Index-Stream.
- Für jeden Index nehmen wir je ein Element aus jeder Liste und „führen sie zusammen“.
- Wir sammeln das Ergebnis mit Collectors.toList() in eine Liste.
Kann man zip für Stream<T> machen?
Technisch — ja, aber bequem nur dann, wenn beide Streams endlich sind und dahinter eine Struktur mit schnellem Indexzugriff steht (also faktisch wieder List). Für „echte“ Streams (zum Beispiel unendliche) ist eine korrekte Implementierung von zip komplizierter und benötigt zusätzliche Logik.
Alternative Varianten: externe Bibliotheken
Wenn Sie ein „fertiges“ zip möchten, können Sie Bibliotheken nutzen:
- org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
- io.vavr.collection.Stream.zip (Vavr)
- com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)
In diesem Kurs bleiben wir bei der Standardbibliothek, daher betrachten wir „manuelle“ Wege.
3. Praktische Anwendungsbeispiele für zip
Beispiel 1. Synchrones Durchlaufen zweier Collections (Summieren der Elemente)
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]
Beispiel 2. Zipping von Strings und Zeichen
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.]
Visualisierung (Schema)
names: [Alice] [Bob] [Charlie]
ages: [20 ] [25 ] [19 ]
| | |
zip ---> (Alice,20) (Bob,25) (Charlie,19)
4. Stream.iterate und Stream.generate — Generierung neuer Streams
Manchmal müssen wir nicht nur bereits existierende Collections verarbeiten, sondern auch neue Sequenzen „on the fly“ erzeugen. Dafür gibt es im Stream API zwei nützliche Methoden:
- Stream.iterate — erzeugt eine Sequenz nach einer Regel (zum Beispiel eine arithmetische Progression).
- Stream.generate — erzeugt einen Stream, bei dem jedes Element über einen Supplier berechnet wird (zum Beispiel Zufallszahl, aktuelle Zeit usw.).
Stream.iterate
Syntax:
Stream.iterate(seed, unaryOperator)
- seed — Startwert;
- unaryOperator — Funktion, die das nächste Element berechnet.
Beispiel 1: Arithmetische Progression
Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// Ausgabe: 0 2 4 6 8
Beispiel 2: Generierung von Datumswerten
import java.time.LocalDate;
Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// Zum Beispiel: 2024-06-09, 2024-06-10, 2024-06-11
Beispiel 3: Unendlicher Stream — vergessen Sie limit nicht!
Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16
In Java 9+ kam eine überladene Variante mit Prädikat-Bedingung hinzu:
Stream.iterate(0, n -> n < 10, n -> n + 2)
.forEach(System.out::println); // 0 2 4 6 8
Stream.generate
Syntax:
Stream.generate(Supplier<T>)
Jedes Element wird durch einen Aufruf von Supplier.get() berechnet.
Beispiel 1: Zufallszahlen
import java.util.Random;
Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);
Beispiel 2: Generierung identischer Werte
Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****
Beispiel 3: Eindeutige Bezeichner
import java.util.UUID;
Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);
Visualisierung (Schema)
Stream.iterate:
[seed] -> op() -> op() -> op() -> ...
n n+1 n+2 n+3
Stream.generate:
Supplier() -> Supplier() -> Supplier() -> ...
val1 val2 val3
5. Anwendungsbeispiele: Datengenerierung für eine Anwendung
Angenommen, wir haben die Klasse Student:
class Student {
String name;
int age;
Student(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
Beispiel 1. Generierung von Test-Studierenden
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Eve");
Stream<Student> students = IntStream.range(0, names.size())
.mapToObj(i -> new Student(names.get(i), 18 + i));
students.forEach(System.out::println);
// Alice (18), Bob (19), Charlie (20), Dave (21), Eve (22)
Beispiel 2. Generierung zufälliger Studierender
Random random = new Random();
List<String> pool = Arrays.asList("Iris", "Oscar", "Max", "Tara", "Sam");
Stream<Student> randomStudents = Stream.generate(() ->
new Student(
pool.get(random.nextInt(pool.size())),
18 + random.nextInt(5)
)
);
randomStudents.limit(3).forEach(System.out::println);
// Zum Beispiel: Tara (19), Oscar (21), Iris (20)
Beispiel 3. Generierung einer Datumsfolge für einen Bericht
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
Vergleich: Wann verwendet man zip, iterate, generate
- zip — wenn zwei (oder mehr) Listen/Streams synchron verarbeitet werden sollen, indem Elemente per Index kombiniert werden.
- iterate — wenn eine Sequenz nach einer Regel benötigt wird (Zahlen, Daten, Schritte).
- generate — wenn jedes Element unabhängig berechnet wird (Zufallswerte, eindeutige IDs).
7. Typische Fehler bei der Arbeit mit zip und der Generierung von Streams
Fehler Nr. 1: Unbegrenzter Stream ohne limit. Wenn Sie Stream.iterate oder Stream.generate ohne Begrenzung via limit verwenden, kann das Programm hängenbleiben oder den gesamten Speicher verbrauchen.
Stream.generate(() -> 1).forEach(System.out::println); // Wird nie enden!
Fehler Nr. 2: Falscher Umgang mit unterschiedlichen Längen beim zip. Wenn eine Liste länger ist als die andere, gehen Sie über die minimale Länge, sonst erhalten Sie eine IndexOutOfBoundsException.
Fehler Nr. 3: Versuch, normale Stream<T> zu zippen. Normale Streams haben keinen Zugriff per Index. Ein praktikables zip macht man häufiger für List.
Fehler Nr. 4: Modifikation einer Collection während der Stream-Generierung. Wenn Sie eine Collection ändern, während ein Stream darüber läuft, kann eine ConcurrentModificationException auftreten. Erzeugen Sie neue Daten — ändern Sie die alten nicht „on the fly“.
Fehler Nr. 5: Verlust der Reihenfolge. Wenn die Reihenfolge wichtig ist (zum Beispiel bei zip), verwenden Sie List, nicht Set — andernfalls ist die Reihenfolge der Elemente unvorhersehbar.
GO TO FULL VERSION