CodeGym /Corsi /JAVA 25 SELF /Zip (zip), generazione di stream (iterate, generate)

Zip (zip), generazione di stream (iterate, generate)

JAVA 25 SELF
Livello 32 , Lezione 3
Disponibile

1. Introduzione a zip

In programmazione il termine zip (o «zippare», «accoppiare») indica un’operazione che prende due (o più) liste e le unisce in un unico stream di coppie: un elemento da ciascuna lista. Se conosci Python, lì c’è la funzione zip che fa esattamente questo.

Esempio:

  • C’è una lista di nomi: ["Anya", "Boris", "Vika"]
  • C’è una lista di età: [20, 25, 19]
  • Dopo lo «zip» si ottiene: [("Anya", 20), ("Boris", 25), ("Vika", 19)]

È comodo quando devi elaborare in modo sincrono due collezioni — per esempio, creare oggetti in cui nome ed età vanno insieme.

Perché nello Stream API non c’è zip?
Nello Stream API standard (fino a Java 22) il metodo zip non c’è. Il motivo è che gli stream possono essere infiniti, le collezioni hanno lunghezze diverse e non è sempre ovvio come comportarsi se una collezione è più lunga dell’altra. Ma nella pratica zip serve spesso.

2. Implementare zip in Java: come vivere senza un metodo incorporato

Il modo più semplice — tramite indici

Se hai due liste e sai per certo che sono collezioni «normali» (per esempio, List<A>, List<B>), puoi usare gli indici:

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 + ")"; }
    }
}

Output:

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

Cosa succede?

  • Prendiamo la dimensione minima tra le due liste — per non uscire dai limiti.
  • Usiamo IntStream.range(0, size) — creiamo uno stream di indici.
  • Per ogni indice prendiamo un elemento da ciascuna lista e li «accoppiamo».
  • Raccogliamo il risultato in una lista con Collectors.toList().

Si può fare zip per Stream<T>?

Tecnicamente sì, ma è comodo solo quando entrambi gli stream sono finiti e dietro c’è una struttura con accesso rapido per indice (cioè in pratica sono List). Per gli «stream veri» (per esempio infiniti) una realizzazione corretta di zip è più complessa e richiede logica aggiuntiva.

Alternative: librerie di terze parti

Se vuoi uno zip «pronto», puoi usare librerie:

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

Nel corso ci atteniamo alla libreria standard, quindi consideriamo approcci «manuali».

3. Esempi pratici d’uso di zip

Esempio 1. Scansione sincrona di due collezioni (somma degli elementi)

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]

Esempio 2. Zip di stringhe e caratteri

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

Visualizzazione (schema)

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

4. Stream.iterate e Stream.generate — generazione di nuovi stream

A volte non serve solo elaborare collezioni già esistenti, ma anche creare nuove sequenze «al volo». Per questo nello Stream API ci sono due metodi utili:

  • Stream.iterate — crea una sequenza secondo una regola (per esempio una progressione aritmetica).
  • Stream.generate — crea uno stream in cui ogni elemento è calcolato tramite un Supplier (per esempio un numero casuale, l’ora corrente, ecc.).

Stream.iterate

Sintassi:

Stream.iterate(seed, unaryOperator)
  • seed — valore iniziale;
  • unaryOperator — funzione che calcola l’elemento successivo.

Esempio 1: Progressione aritmetica

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

Esempio 2: Generazione di date

import java.time.LocalDate;

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

Esempio 3: Stream infinito — non dimenticare 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+ è stata introdotta una variante sovraccaricata con un predicato di condizione:

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

Stream.generate

Sintassi:

Stream.generate(Supplier<T>)

Ogni elemento viene calcolato chiamando Supplier.get().

Esempio 1: Numeri casuali

import java.util.Random;

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

Esempio 2: Generazione di valori identici

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

Esempio 3: Identificatori unici

import java.util.UUID;

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

Visualizzazione (schema)

Stream.iterate:

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

Stream.generate:

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

5. Esempi d’uso: generazione di dati per un’applicazione

Supponiamo di avere la classe Student:

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

Esempio 1. Generazione di studenti di test

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)

Esempio 2. Generazione di studenti casuali

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);
// Ad esempio: Tanya (19), Oleg (21), Ira (20)

Esempio 3. Generazione di una sequenza di date per un 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

Confronto: quando usare zip, iterate, generate

  • zip — quando devi elaborare in modo sincrono due (o più) liste/stream, unendo gli elementi per indice.
  • iterate — quando serve una sequenza secondo una regola (numeri, date, passi).
  • generate — quando ogni elemento è calcolato indipendentemente (valori casuali, ID unici).

7. Errori tipici con zip e generazione di stream

Errore n. 1: Stream non limitato senza limit. Se usi Stream.iterate o Stream.generate senza limitarli con limit, il programma può bloccarsi o consumare tutta la memoria.

Stream.generate(() -> 1).forEach(System.out::println); // Non terminerà mai!

Errore n. 2: Gestione errata di lunghezze diverse in zip. Se una lista è più lunga dell’altra, bisogna iterare fino alla lunghezza minima, altrimenti otterrai IndexOutOfBoundsException.

Errore n. 3: Tentare di fare zip su Stream<T> normali. Gli stream normali non hanno accesso per indice. Uno zip pratico si fa più spesso con List.

Errore n. 4: Modificare una collezione durante la generazione dello stream. Se modifichi la collezione mentre la stai attraversando con uno stream, puoi ottenere una ConcurrentModificationException. Genera nuovi dati — non modificare «al volo» quelli esistenti.

Errore n. 5: Perdita dell’ordine. Se l’ordine è importante (per esempio con zip), usa List, non Set — altrimenti l’ordine degli elementi sarà imprevedibile.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION