JavaRush /Курси /JAVA 25 SELF /Zip-ування (zip), генерація потоків (iterate, generate)

Zip-ування (zip), генерація потоків (iterate, generate)

JAVA 25 SELF
Рівень 32 , Лекція 3
Відкрита

1. Знайомство з zip

У програмуванні термін zip (або «zip-ування», «зшивання») означає операцію, що бере два (або більше) списки й об’єднує їх в один потік пар: по одному елементу з кожного списку. Якщо ви знайомі з Python, там є функція zip, яка робить саме це.

Приклад:

  • Є список імен: ["Аня", "Борис", "Віка"]
  • Є список віків: [20, 25, 19]
  • Після «zip-ування» отримаємо: [("Аня", 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. Zip-ування рядків і символів

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 — коли кожен елемент обчислюється незалежно (випадкові значення, унікальні ідентифікатори).

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 — інакше порядок елементів буде непередбачуваним.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ