1. Stream.concat: combinamos dos streams
En programación a menudo surge la situación: tenemos dos (o más) streams de datos y queremos combinarlos en uno solo. Por ejemplo, dos listas de estudiantes de grupos distintos — y necesitamos procesarlos todos a la vez. En los «viejos tiempos» (antes de Stream API) simplemente habríamos unido dos listas con addAll. Pero si trabajamos con streams (Stream<T>), queremos hacerlo de forma perezosa y expresiva.
Sintaxis y principio de funcionamiento
La forma más básica de unir dos streams es usar el método estático Stream.concat:
Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
Qué ocurre:
- Primero se toma el primer stream y después el segundo.
- La salida es un nuevo stream que primero emite todos los elementos del primero y luego todos los del segundo.
- La unión es perezosa: hasta que no empieces a recorrer el stream resultante con una operación terminal (por ejemplo, forEach o collect), no ocurrirá nada.
Ejemplo: unión de dos listas de nombres
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris", "Vika");
List<String> groupB = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());
allStudents.forEach(System.out::println);
Resultado:
Anya
Boris
Vika
Grisha
Dasha
Importante: tras la unión, el stream resultante es de un solo uso. Como cualquier otro Stream, no se puede reutilizar.
Particularidades de Stream.concat
- Solo dos streams a la vez. Para tres o más — o en cadena con concat, o bien otros enfoques (ver abajo).
- El orden se conserva. Primero los elementos del primer stream, luego los del segundo.
- Unión perezosa. Si el primer stream es infinito, nunca se alcanzará el segundo.
- Bug típico: intentar unir un stream consigo mismo (Stream.concat(stream, stream)) conlleva reutilizar el mismo stream — está prohibido.
2. Unión de más de dos streams: flatMap y Stream.of
Cuando hay más de dos streams, resulta más cómodo combinarlos con la combinación Stream.of + flatMap.
Usar Stream.of + flatMap
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris");
List<String> groupB = List.of("Vika");
List<String> groupC = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
.flatMap(Collection::stream);
allStudents.forEach(System.out::println);
Qué ocurre aquí:
- Stream.of(groupA, groupB, groupC) crea un stream de tres colecciones.
- flatMap(Collection::stream) despliega cada colección en un único stream de cadenas.
- Resultado — un único gran stream con todos los estudiantes.
Ventaja: se pueden unir tantos streams como se quiera.
Otra opción: unir una lista de streams
import java.util.List;
import java.util.stream.Stream;
List<Stream<String>> streams = List.of(
Stream.of("a", "b"),
Stream.of("c"),
Stream.of("d", "e")
);
Stream<String> merged = streams.stream()
.flatMap(s -> s);
merged.forEach(System.out::println);
Resultado:
a
b
c
d
e
3. Collectors.joining: unimos cadenas con separador
A veces no basta con unir streams, sino que hay que pegar todos los elementos en una sola cadena — por ejemplo, para mostrar los nombres separados por comas. Para ello está el collector Collectors.joining.
Sintaxis
String result = stream.collect(Collectors.joining(", "));
- Sin argumentos: pegará las cadenas sin separador.
- Con separador: entre elementos habrá la cadena indicada.
- Con prefijo y sufijo: Collectors.joining(delimiter, prefix, suffix)
Ejemplo: lista de estudiantes en una sola cadena
import java.util.List;
import java.util.stream.Collectors;
List<String> students = List.of("Anya", "Boris", "Vika");
String line = students.stream()
.collect(Collectors.joining(", "));
System.out.println("Lista de estudiantes: " + line);
Resultado:
Lista de estudiantes: Anya, Boris, Vika
Ejemplo con prefijo y sufijo
String line = students.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(line);
Resultado:
[Anya, Boris, Vika]
¿Para qué sirve?
- Generar cadenas CSV.
- Impresión bonita de listas e informes.
- Pasar datos en una sola cadena (p. ej., en una URL o en un log).
4. Comparación de concat y flatMap: ¿cuándo usar cada uno?
- Stream.concat — cuando hay exactamente dos streams e importa conservar el orden.
- Stream.of + flatMap — cuando hay muchos streams o están en una colección.
- Collectors.joining — cuando los elementos son de tipo String y se necesita un resultado en forma de una sola cadena.
Tabla comparativa
| Método | Cuándo usar | Ejemplo de código |
|---|---|---|
|
Dos streams | |
|
Muchos streams, colección de streams | |
|
Unir elementos en una sola cadena | |
5. Ejemplos prácticos en una aplicación educativa
Ejemplo 1. Unimos estudiantes de dos facultades
import java.util.List;
import java.util.stream.Stream;
List<String> itStudents = List.of("Anya", "Boris");
List<String> mathStudents = List.of("Vika", "Grisha");
Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());
all.forEach(System.out::println);
Ejemplo 2. Unimos estudiantes de todos los grupos y los mostramos en una sola cadena
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
List<List<String>> allGroups = List.of(
List.of("Anya", "Boris"),
List.of("Vika"),
List.of("Grisha", "Dasha")
);
String allNames = allGroups.stream()
.flatMap(Collection::stream)
.collect(Collectors.joining("; "));
System.out.println("Todos los estudiantes: " + allNames);
Resultado:
Todos los estudiantes: Anya; Boris; Vika; Grisha; Dasha
Ejemplo 3. Unimos streams de números
import java.util.stream.Stream;
Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.of(4, 5);
Stream<Integer> merged = Stream.concat(s1, s2);
merged.forEach(System.out::print); // 12345
6. Matices y particularidades importantes
concat solo funciona con dos streams
Para tres streams se obtiene una escritura anidada — funciona, pero es ruidosa. Es mejor usar Stream.of + flatMap:
Stream<Integer> s1 = Stream.of(1);
Stream<Integer> s2 = Stream.of(2);
Stream<Integer> s3 = Stream.of(3);
Stream<Integer> merged = Stream.concat(Stream.concat(s1, s2), s3);
Los streams son de un solo uso
Después de una operación terminal no se puede usar el stream. Reutilizarlo provoca IllegalStateException.
concat es perezoso
Si el primer stream es infinito (por ejemplo, creado con Stream.generate(...)), nunca se alcanzará el segundo.
Orden de los elementos
El orden siempre se conserva: primero el primer stream y luego el segundo.
Collectors.joining solo funciona con Stream<String>
Para tipos que no son cadenas, primero convierte los elementos a cadenas, por ejemplo, con map(Object::toString):
import java.util.stream.Collectors;
import java.util.stream.Stream;
Stream<Integer> numbers = Stream.of(1, 2, 3);
String line = numbers
.map(Object::toString)
.collect(Collectors.joining(", "));
System.out.println(line); // 1, 2, 3
Comparación con el enfoque «manual»
Antes de Stream API:
import java.util.ArrayList;
import java.util.List;
List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);
Con Stream API:
import java.util.stream.Collectors;
List<String> merged = Stream.concat(listA.stream(), listB.stream())
.collect(Collectors.toList());
Ventaja de los streams: se pueden encadenar operaciones intermedias (filter, map, etc.) antes y después de la unión, además de trabajar con cualquier fuente de datos.
7. Errores típicos al unir streams
Error n.º 1: reutilización del stream. Un stream solo puede usarse una vez. No se puede unir un stream consigo mismo.
import java.util.stream.Stream;
Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // ¡Error! s se reutilizará
Error n.º 2: intentar unir un stream infinito con uno finito. Si el primer stream es infinito, el segundo nunca empezará.
import java.util.stream.Stream;
Stream<Integer> infinite = Stream.generate(() -> 1);
Stream<Integer> finite = Stream.of(2, 3);
Stream<Integer> merged = Stream.concat(infinite, finite);
// merged.limit(10).forEach(System.out::println); // finite no entrará en el resultado
Error n.º 3: joining con tipos que no son cadenas. El collector Collectors.joining acepta un stream de cadenas. Para números hay que convertirlos previamente a cadenas:
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // ¡Error de compilación!
String joined = numbers.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
Error n.º 4: pérdida de orden. Si el orden es importante, usa concat o un flatMap ordenado. Métodos como unordered() pueden romper el orden determinista de salida.
GO TO FULL VERSION