1. Introduction to zip
In programming, the term zip (or “zipping”, “stitching”) refers to an operation that takes two (or more) lists and combines them into one stream of pairs: one element from each list at a time. If you are familiar with Python, it has a zip function that does exactly this.
Example:
- There is a list of names: ["Alice", "Bob", "Victoria"]
- There is a list of ages: [20, 25, 19]
- After zipping you get: [("Alice", 20), ("Bob", 25), ("Victoria", 19)]
This is handy when you need to process two collections in sync—for example, to create objects where name and age go together.
Why isn’t there zip in the Stream API?
The standard Stream API (up to Java 22) has no zip method. The reason is that streams can be infinite, collections can have different lengths, and it’s not always obvious how to behave if one collection is longer than the other. But in practice, zip is often needed.
2. Implementing zip in Java: how to live without a built-in method
The simplest way—via indexes
If you have two lists and you know for sure they are “regular” collections (for example, List<A>, List<B>), you can use indexes:
import java.util.*;
import java.util.stream.*;
public class ZipExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "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 + ")"; }
}
}
Output:
Alice (20)
Bob (25)
Victoria (19)
What’s happening?
- We take the minimal size of the two lists so we don’t go out of bounds.
- We use IntStream.range(0, size) to create a stream of indexes.
- For each index, we take one element from each list and stitch them together.
- We collect the result into a list via Collectors.toList().
Can we implement zip for Stream<T>?
Technically—yes, but it’s convenient only when both streams are finite and backed by a structure with fast random access (that is, effectively the same List). For “real” streams (e.g., infinite ones), a correct zip implementation is more complex and requires additional logic.
Alternative options: third‑party libraries
If you want a “ready-made” zip, you can use libraries:
- org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
- io.vavr.collection.Stream.zip (Vavr)
- com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)
In this course we stick to the standard library, so we consider “manual” approaches.
3. Practical examples of using zip
Example 1. Synchronous traversal of two collections (summing elements)
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]
Example 2. Zipping strings and characters
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 (diagram)
names: [Alice] [Bob] [Victoria]
ages: [20 ] [25 ] [19 ]
| | |
zip ---> (Alice,20) (Bob,25) (Victoria,19)
4. Stream.iterate and Stream.generate—generating new streams
Sometimes we not only need to process existing collections, but also create new sequences on the fly. For this, the Stream API has two useful methods:
- Stream.iterate—creates a sequence by a rule (for example, an arithmetic progression).
- Stream.generate—creates a stream where each element is computed via a Supplier (for example, a random number, the current time, etc.).
Stream.iterate
Syntax:
Stream.iterate(seed, unaryOperator)
- seed—the starting value;
- unaryOperator—a function that computes the next element.
Example 1: Arithmetic progression
Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// Output: 0 2 4 6 8
Example 2: Generating dates
import java.time.LocalDate;
Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// For example: 2024-06-09, 2024-06-10, 2024-06-11
Example 3: Infinite stream—don’t forget limit!
Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16
In Java 9+ an overloaded variant with a predicate condition appeared:
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>)
Each element is computed by calling Supplier.get().
Example 1: Random numbers
import java.util.Random;
Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);
Example 2: Generating identical values
Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****
Example 3: Unique identifiers
import java.util.UUID;
Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);
Visualisation (diagram)
Stream.iterate:
[seed] -> op() -> op() -> op() -> ...
n n+1 n+2 n+3
Stream.generate:
Supplier() -> Supplier() -> Supplier() -> ...
val1 val2 val3
5. Usage examples: generating app data
Suppose we have a Student class:
class Student {
String name;
int age;
Student(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
Example 1. Generating test students
List<String> names = Arrays.asList("Alice", "Bob", "Victoria", "George", "Daisy");
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), Victoria (20), George (21), Daisy (22)
Example 2. Generating random students
Random random = new Random();
List<String> pool = Arrays.asList("Irene", "Oliver", "Max", "Tina", "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);
// For example: Tina (19), Oliver (21), Irene (20)
Example 3. Generating a sequence of dates for a report
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
Comparison: when to use zip, iterate, generate
- zip—when you need to synchronously process two (or more) lists/streams by joining elements by index.
- iterate—when you need a sequence by a given rule (numbers, dates, steps).
- generate—when each element is computed independently (random values, unique IDs).
6. Common mistakes when working with zip and stream generation
Error No. 1: Unbounded stream without limit. If you use Stream.iterate or Stream.generate without limiting via limit, the program may hang or eat all the memory.
Stream.generate(() -> 1).forEach(System.out::println); // Will never finish!
Error No. 2: Improper handling of different lengths when zipping. If one list is longer than the other, iterate up to the minimal length, otherwise you’ll get IndexOutOfBoundsException.
Error No. 3: Attempting to zip ordinary Stream<T>. Plain streams do not have random access by index. A practical zip is usually done for List.
Error No. 4: Modifying a collection while generating a stream. If you change a collection while a stream is iterating over it, you can get a ConcurrentModificationException. Generate new data—do not mutate the old ones on the fly.
Error No. 5: Losing order. If order matters (for example, with zip), use a List, not a Set—otherwise the element order will be unpredictable.
GO TO FULL VERSION