CodeGym /Cursos /JAVA 25 SELF /Zip (zip), generación de flujos (iterate, generate)

Zip (zip), generación de flujos (iterate, generate)

JAVA 25 SELF
Nivel 32 , Lección 3
Disponible

1. Introducción a zip

En programación, el término zip (o «emparejamiento», «costura») es una operación que toma dos (o más) listas y las combina en un único stream de pares: un elemento de cada lista. Si conoces Python, allí existe la función zip, que hace exactamente eso.

Ejemplo:

  • Hay una lista de nombres: ["Anya", "Boris", "Vika"]
  • Hay una lista de edades: [20, 25, 19]
  • Después de aplicar «zip» obtendremos: [("Anya", 20), ("Boris", 25), ("Vika", 19)]

Es útil cuando necesitas procesar dos colecciones en sincronía — por ejemplo, crear objetos en los que el nombre y la edad vayan juntos.

¿Por qué no hay zip en el Stream API?
En el Stream API estándar (hasta Java 22) no existe el método zip. La razón es que los streams pueden ser infinitos, las colecciones tener longitudes distintas, y no siempre es evidente cómo actuar si una colección es más larga que la otra. Aun así, en la práctica zip se necesita a menudo.

2. Implementación de zip en Java: cómo vivir sin un método integrado

La forma más sencilla — mediante índices

Si tienes dos listas y sabes con certeza que son colecciones «normales» (por ejemplo, List<A>, List<B>), puedes usar índices:

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

Salida:

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

¿Qué está pasando?

  • Tomamos el tamaño mínimo de las dos listas — para no salirnos de los límites.
  • Usamos IntStream.range(0, size) — creamos un stream de índices.
  • Para cada índice tomamos un elemento de cada lista y los «emparejamos».
  • Recogemos el resultado en una lista con Collectors.toList().

¿Se puede hacer zip para Stream<T>?

Técnicamente — sí, pero es cómodo solo cuando ambos streams son finitos y detrás de ellos hay una estructura con acceso rápido por índice (es decir, en la práctica son List). Para streams «reales» (por ejemplo, infinitos) una implementación correcta de zip es más compleja y requiere lógica adicional.

Alternativas: bibliotecas de terceros

Si quieres un zip «listo para usar», puedes recurrir a bibliotecas:

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

En el curso nos atenemos a la biblioteca estándar, por lo que veremos métodos «manuales».

3. Ejemplos prácticos de uso de zip

Ejemplo 1. Recorrido sincrónico de dos colecciones (suma de elementos)

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]

Ejemplo 2. Zip de cadenas y caracteres

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

Visualización (esquema)

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

4. Stream.iterate y Stream.generate — generación de nuevos streams

A veces no solo necesitamos procesar colecciones ya existentes, sino también crear nuevas secuencias «sobre la marcha». Para ello, en el Stream API hay dos métodos útiles:

  • Stream.iterate — crea una secuencia basada en una regla (por ejemplo, una progresión aritmética).
  • Stream.generate — crea un stream donde cada elemento se calcula mediante un Supplier (por ejemplo, un número aleatorio, la hora actual, etc.).

Stream.iterate

Sintaxis:

Stream.iterate(seed, unaryOperator)
  • seed — valor inicial;
  • unaryOperator — función que calcula el siguiente elemento.

Ejemplo 1: Progresión aritmética

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

Ejemplo 2: Generación de fechas

import java.time.LocalDate;

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

Ejemplo 3: Stream infinito — ¡no olvides limit!

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

En Java 9+ apareció una variante sobrecargada con una condición (predicado):

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

Stream.generate

Sintaxis:

Stream.generate(Supplier<T>)

Cada elemento se calcula invocando Supplier.get().

Ejemplo 1: Números aleatorios

import java.util.Random;

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

Ejemplo 2: Generación de valores idénticos

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

Ejemplo 3: Identificadores únicos

import java.util.UUID;

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

Visualización (esquema)

Stream.iterate:

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

Stream.generate:

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

5. Ejemplos de uso: generación de datos para una aplicación

Supongamos que tenemos la clase Student:

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

Ejemplo 1. Generación de estudiantes de prueba

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)

Ejemplo 2. Generación de estudiantes aleatorios

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

Ejemplo 3. Generación de una secuencia de fechas para un informe

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

Comparativa: cuándo usar zip, iterate, generate

  • zip — cuando necesitas procesar en sincronía dos (o más) listas/streams, uniendo elementos por índices.
  • iterate — cuando necesitas una secuencia definida por una regla (números, fechas, pasos).
  • generate — cuando cada elemento se calcula de forma independiente (valores aleatorios, ID únicos).

7. Errores típicos al trabajar con zip y la generación de streams

Error n.º 1: Stream sin limitar con limit. Si usas Stream.iterate o Stream.generate sin limitar con limit, el programa puede colgarse o «comerse» toda la memoria.

Stream.generate(() -> 1).forEach(System.out::println); // ¡Nunca terminará!

Error n.º 2: Manejo incorrecto de longitudes distintas en zip. Si una lista es más larga que la otra, hay que iterar hasta la longitud mínima; de lo contrario obtendrás IndexOutOfBoundsException.

Error n.º 3: Intentar hacer zip de Stream<T> normales. Los streams normales no tienen acceso por índice. El zip práctico suele hacerse con List.

Error n.º 4: Modificar una colección mientras se genera un stream. Si cambias la colección mientras se itera en un stream sobre ella, puedes obtener ConcurrentModificationException. Genera datos nuevos — no cambies los antiguos «sobre la marcha».

Error n.º 5: Pérdida del orden. Si el orden es importante (por ejemplo, con zip), usa List, no Set — de lo contrario el orden de los elementos será impredecible.

1
Tarea
JAVA 25 SELF, nivel 32, lección 3
Bloqueada
Creación de etiquetas para productos con códigos limitados 🏷️
Creación de etiquetas para productos con códigos limitados 🏷️
1
Tarea
JAVA 25 SELF, nivel 32, lección 3
Bloqueada
Generación de perfiles de prueba para una red social 👥
Generación de perfiles de prueba para una red social 👥
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION