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.
GO TO FULL VERSION