1. Знакомство с zip
В программировании под термином zip (или «зипование», «сшивка») понимается операция, которая берёт два (или более) списка и объединяет их в один поток пар: по одному элементу из каждого списка. Если вы знакомы с Python, там есть функция zip, которая делает именно это.
Пример:
- Есть список имён: ["Аня", "Борис", "Вика"]
- Есть список возрастов: [20, 25, 19]
- После «зипования» получится: [("Аня", 20), ("Борис", 25), ("Вика", 19)]
Это удобно, когда нужно синхронно обрабатывать две коллекции — например, создавать объекты, в которых имя и возраст идут вместе.
Почему в Stream API нет zip?
В стандартном Stream API (до Java 22) метода zip нет. Причина в том, что стримы могут быть бесконечными, коллекции — разной длины, и не всегда очевидно, как вести себя, если одна коллекция длиннее другой. Но на практике zip нужен часто.
2. Реализация zip на Java: как жить без встроенного метода
Самый простой способ — через индексы
Если у вас есть два списка, и вы точно знаете, что это «обычные» коллекции (например, List<A>, List<B>), можно воспользоваться индексами:
import java.util.*;
import java.util.stream.*;
public class ZipExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Аня", "Борис", "Вика");
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 + ")"; }
}
}
Вывод:
Аня (20)
Борис (25)
Вика (19)
Что происходит?
- Берём минимальный размер из двух списков — чтобы не выйти за границы.
- Используем IntStream.range(0, size) — создаём поток индексов.
- Для каждого индекса берём по одному элементу из каждого списка и «сшиваем» их.
- Собираем результат в список через Collectors.toList().
Можно ли сделать zip для Stream<T>?
Технически — да, но удобно только тогда, когда оба стрима конечные и за ними стоит структура с быстрым доступом по индексу (то есть фактически это те же List). Для «настоящих» потоков (например, бесконечных) корректная реализация zip сложнее и требует дополнительной логики.
Альтернативные варианты: сторонние библиотеки
Если хочется «готовый» zip, можно использовать библиотеки:
- org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
- io.vavr.collection.Stream.zip (Vavr)
- com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)
В курсе придерживаемся стандартной библиотеки, поэтому рассматриваем «ручные» способы.
3. Практические примеры использования zip
Пример 1. Синхронный обход двух коллекций (суммирование элементов)
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]
Пример 2. Зипование строк и символов
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.]
Визуализация (схема)
names: [Аня] [Борис] [Вика]
ages: [20 ] [25 ] [19 ]
| | |
zip ---> (Аня,20) (Борис,25) (Вика,19)
4. Stream.iterate и Stream.generate — генерация новых потоков
Иногда нам нужно не только обрабатывать уже существующие коллекции, но и создавать новые последовательности «на лету». Для этого в Stream API есть два полезных метода:
- Stream.iterate — создаёт последовательность по правилу (например, арифметическая прогрессия).
- Stream.generate — создаёт поток, где каждый элемент вычисляется через Supplier (например, случайное число, текущее время и т. д.).
Stream.iterate
Синтаксис:
Stream.iterate(seed, unaryOperator)
- seed — стартовое значение;
- unaryOperator — функция, вычисляющая следующий элемент.
Пример 1: Арифметическая прогрессия
Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// Выведет: 0 2 4 6 8
Пример 2: Генерация дат
import java.time.LocalDate;
Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// Например: 2024-06-09, 2024-06-10, 2024-06-11
Пример 3: Бесконечный поток — не забывайте limit!
Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16
В Java 9+ появился перегруженный вариант с предикатом-условием:
Stream.iterate(0, n -> n < 10, n -> n + 2)
.forEach(System.out::println); // 0 2 4 6 8
Stream.generate
Синтаксис:
Stream.generate(Supplier<T>)
Каждый элемент вычисляется вызовом Supplier.get().
Пример 1: Случайные числа
import java.util.Random;
Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);
Пример 2: Генерация одинаковых значений
Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****
Пример 3: Уникальные идентификаторы
import java.util.UUID;
Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);
Визуализация (схема)
Stream.iterate:
[seed] -> op() -> op() -> op() -> ...
n n+1 n+2 n+3
Stream.generate:
Supplier() -> Supplier() -> Supplier() -> ...
val1 val2 val3
5. Примеры использования: генерация данных для приложения
Пусть у нас есть класс Student:
class Student {
String name;
int age;
Student(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
Пример 1. Генерация тестовых студентов
List<String> names = Arrays.asList("Аня", "Борис", "Вика", "Глеб", "Даша");
Stream<Student> students = IntStream.range(0, names.size())
.mapToObj(i -> new Student(names.get(i), 18 + i));
students.forEach(System.out::println);
// Аня (18), Борис (19), Вика (20), Глеб (21), Даша (22)
Пример 2. Генерация случайных студентов
Random random = new Random();
List<String> pool = Arrays.asList("Ира", "Олег", "Максим", "Таня", "Сергей");
Stream<Student> randomStudents = Stream.generate(() ->
new Student(
pool.get(random.nextInt(pool.size())),
18 + random.nextInt(5)
)
);
randomStudents.limit(3).forEach(System.out::println);
// Например: Таня (19), Олег (21), Ира (20)
Пример 3. Генерация последовательности дат для отчёта
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
Сравнение: когда использовать zip, iterate, generate
- zip — когда нужно синхронно обработать два (или более) списка/потока, объединяя элементы по индексам.
- iterate — когда нужна последовательность по заданному правилу (числа, даты, шаги).
- generate — когда каждый элемент вычисляется независимо (случайные значения, уникальные ID).
7. Типичные ошибки при работе с zip и генерацией потоков
Ошибка №1: Неограниченный поток без limit. Если вы используете Stream.iterate или Stream.generate без ограничения через limit, программа может зависнуть или «съесть» всю память.
Stream.generate(() -> 1).forEach(System.out::println); // Никогда не закончится!
Ошибка №2: Неправильная обработка разных длин при zip. Если один список длиннее другого, идти нужно по минимальной длине, иначе получите IndexOutOfBoundsException.
Ошибка №3: Попытка zip-ить обычные Stream<T>. У обычных стримов нет доступа по индексу. Практичный zip чаще делают для List.
Ошибка №4: Модификация коллекции во время генерации потока. Если менять коллекцию, пока по ней идёт стрим, можно получить ConcurrentModificationException. Генерируйте новые данные — не меняйте старые «на лету».
Ошибка №5: Потеря порядка. Если порядок важен (например, при zip), используйте List, не Set — иначе порядок элементов окажется непредсказуемым.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ