CodeGym /Kursy /JAVA 25 SELF /Zipowanie (zip), generowanie strumieni (iterate, generate...

Zipowanie (zip), generowanie strumieni (iterate, generate)

JAVA 25 SELF
Poziom 32, Lekcja 3
Dostępny

1. Wprowadzenie do zip

W programowaniu pod terminem zip (czyli „zipowanie”, „zszywanie”) rozumiemy operację, która bierze dwie (lub więcej) listy i łączy je w jeden strumień par: po jednym elemencie z każdej listy. Jeśli znasz Pythona, jest tam funkcja zip, która robi dokładnie to.

Przykład:

  • Jest lista imion: ["Anya", "Boris", "Vika"]
  • Jest lista wieków: [20, 25, 19]
  • Po „zipowaniu” otrzymamy: [("Anya", 20), ("Boris", 25), ("Vika", 19)]

To jest wygodne, gdy trzeba synchronicznie przetwarzać dwie kolekcje — na przykład tworzyć obiekty, w których imię i wiek występują razem.

Dlaczego w Stream API nie ma zip?
W standardowym Stream API (do Java 22) metody zip nie ma. Powód jest taki, że strumienie mogą być nieskończone, kolekcje — różnej długości, i nie zawsze oczywiste jest, jak się zachować, gdy jedna kolekcja jest dłuższa od drugiej. W praktyce jednak zip jest często potrzebny.

2. Implementacja zip w Javie: jak żyć bez wbudowanej metody

Najprostszy sposób — przez indeksy

Jeśli masz dwie listy i wiesz na pewno, że to „zwykłe” kolekcje (na przykład List<A>, List<B>), można skorzystać z indeksów:

import java.util.*;
import java.util.stream.*;

public class ZipExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Anya", "Boris", "Vika");
        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 + ")"; }
    }
}

Wyjście:

Anya (20)
Boris (25)
Vika (19)

Co się dzieje?

  • Bierzemy minimalny rozmiar z dwóch list — aby nie wyjść poza zakres.
  • Używamy IntStream.range(0, size) — tworzymy strumień indeksów.
  • Dla każdego indeksu bierzemy po jednym elemencie z każdej listy i „zszywamy” je.
  • Zbieramy wynik do listy przez Collectors.toList().

Czy można zrobić zip dla Stream<T>?

Technicznie — tak, ale jest to wygodne tylko wtedy, gdy oba strumienie są skończone i stoi za nimi struktura z szybkim dostępem po indeksie (czyli faktycznie to te same List). Dla „prawdziwych” strumieni (na przykład nieskończonych) poprawna implementacja zip jest trudniejsza i wymaga dodatkowej logiki.

Alternatywy: biblioteki zewnętrzne

Jeśli chcesz „gotowe” zip, możesz użyć bibliotek:

  • org.apache.commons.lang3.Streams.zip (Apache Commons Lang 3.10+)
  • io.vavr.collection.Stream.zip (Vavr)
  • com.codepoetics.protonpack.StreamUtils.zip (ProtonPack)

W kursie trzymamy się standardowej biblioteki, więc omawiamy „ręczne” sposoby.

3. Praktyczne przykłady użycia zip

Przykład 1. Synchroniczne przejście po dwóch kolekcjach (sumowanie elementów)

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]

Przykład 2. Zipowanie napisów i znaków

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.]

Wizualizacja (schemat)

names:   [Anya] [Boris] [Vika]
ages:    [20 ] [25   ] [19  ]
          |      |      |
zip ---> (Anya,20) (Boris,25) (Vika,19)

4. Stream.iterate i Stream.generate — generowanie nowych strumieni

Czasami musimy nie tylko przetwarzać już istniejące kolekcje, ale i tworzyć nowe sekwencje „w locie”. Do tego w Stream API są dwa użyteczne metody:

  • Stream.iterate — tworzy sekwencję według reguły (np. postęp arytmetyczny).
  • Stream.generate — tworzy strumień, w którym każdy element obliczany jest przez Supplier (np. losowa liczba, bieżący czas itp.).

Stream.iterate

Składnia:

Stream.iterate(seed, unaryOperator)
  • seed — wartość początkowa;
  • unaryOperator — funkcja wyliczająca kolejny element.

Przykład 1: Postęp arytmetyczny

Stream<Integer> numbers = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
numbers.limit(5).forEach(System.out::println);
// Wypisze: 0 2 4 6 8

Przykład 2: Generowanie dat

import java.time.LocalDate;

Stream<LocalDate> days = Stream.iterate(LocalDate.now(), date -> date.plusDays(1));
days.limit(3).forEach(System.out::println);
// Na przykład: 2024-06-09, 2024-06-10, 2024-06-11

Przykład 3: Nieskończony strumień — nie zapominaj o limit!

Stream<Integer> endless = Stream.iterate(1, n -> n * 2);
endless.limit(5).forEach(System.out::println); // 1 2 4 8 16

W Java 9+ pojawiła się przeładowana wersja z predykatem-warunkiem:

Stream.iterate(0, n -> n < 10, n -> n + 2)
    .forEach(System.out::println); // 0 2 4 6 8

Stream.generate

Składnia:

Stream.generate(Supplier<T>)

Każdy element jest obliczany poprzez wywołanie Supplier.get().

Przykład 1: Liczby losowe

import java.util.Random;

Random random = new Random();
Stream<Integer> randoms = Stream.generate(random::nextInt);
randoms.limit(5).forEach(System.out::println);

Przykład 2: Generowanie identycznych wartości

Stream<String> stars = Stream.generate(() -> "*");
stars.limit(4).forEach(System.out::print); // ****

Przykład 3: Unikalne identyfikatory

import java.util.UUID;

Stream<String> uuids = Stream.generate(() -> UUID.randomUUID().toString());
uuids.limit(3).forEach(System.out::println);

Wizualizacja (schemat)

Stream.iterate:

[seed] -> op() -> op() -> op() -> ...
       n      n+1    n+2    n+3

Stream.generate:

Supplier() -> Supplier() -> Supplier() -> ...
    val1         val2          val3

5. Przykłady użycia: generowanie danych dla aplikacji

Załóżmy, że mamy klasę Student:

class Student {
    String name;
    int age;
    Student(String name, int age) { this.name = name; this.age = age; }
    public String toString() { return name + " (" + age + ")"; }
}

Przykład 1. Generowanie testowych studentów

List<String> names = Arrays.asList("Anya", "Boris", "Vika", "Gleb", "Dasha");
Stream<Student> students = IntStream.range(0, names.size())
    .mapToObj(i -> new Student(names.get(i), 18 + i));

students.forEach(System.out::println);
// Anya (18), Boris (19), Vika (20), Gleb (21), Dasha (22)

Przykład 2. Generowanie losowych studentów

Random random = new Random();
List<String> pool = Arrays.asList("Ira", "Oleg", "Maksim", "Tanya", "Sergey");

Stream<Student> randomStudents = Stream.generate(() ->
    new Student(
        pool.get(random.nextInt(pool.size())),
        18 + random.nextInt(5)
    )
);

randomStudents.limit(3).forEach(System.out::println);
// Na przykład: Tanya (19), Oleg (21), Ira (20)

Przykład 3. Generowanie sekwencji dat do raportu

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

Porównanie: kiedy używać zip, iterate, generate

  • zip — gdy trzeba synchronicznie przetworzyć dwie (lub więcej) listy/strumienie, łącząc elementy po indeksach.
  • iterate — gdy potrzebna jest sekwencja według zadanej reguły (liczby, daty, kroki).
  • generate — gdy każdy element jest obliczany niezależnie (wartości losowe, unikalne ID).

6. Typowe błędy przy pracy z zip i generowaniem strumieni

Błąd nr 1: Nieograniczony strumień bez limit. Jeśli użyjesz Stream.iterate lub Stream.generate bez ograniczenia przez limit, program może się zawiesić albo „zjeść” całą pamięć.

Stream.generate(() -> 1).forEach(System.out::println); // Nigdy się nie zakończy!

Błąd nr 2: Niepoprawne obsłużenie różnych długości przy zip. Jeśli jedna lista jest dłuższa od drugiej, należy iść po minimalnej długości, w przeciwnym razie dostaniesz IndexOutOfBoundsException.

Błąd nr 3: Próba zipowania zwykłych Stream<T>. Zwykłe strumienie nie mają dostępu po indeksie. Praktyczny zip częściej robi się dla List.

Błąd nr 4: Modyfikowanie kolekcji podczas generowania strumienia. Jeśli zmieniasz kolekcję, gdy jest nad nią wykonywany stream, możesz dostać ConcurrentModificationException. Generuj nowe dane — nie zmieniaj starych „w locie”.

Błąd nr 5: Utrata kolejności. Jeśli kolejność jest ważna (na przykład przy zip), używaj List, nie Set — w przeciwnym razie kolejność elementów okaże się nieprzewidywalna.

1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 3
Niedostępne
Tworzenie etykiet dla produktów z ograniczonymi kodami 🏷️
Tworzenie etykiet dla produktów z ograniczonymi kodami 🏷️
1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 3
Niedostępne
Generowanie testowych profili dla sieci społecznościowej 👥
Generowanie testowych profili dla sieci społecznościowej 👥
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION