CodeGym /Cursos /JAVA 25 SELF /Métodos join, concat: combinación de streams

Métodos join, concat: combinación de streams

JAVA 25 SELF
Nivel 32, Lección 1
Disponible

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
Stream.concat(a, b)
Dos streams
Stream.concat(a, b)
Stream.of(...).flatMap()
Muchos streams, colección de streams
Stream.of(a, b, c).flatMap(x -> x)
Collectors.joining()
Unir elementos en una sola cadena
stream.collect(Collectors.joining(", "))

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.

1
Tarea
JAVA 25 SELF, nivel 32, lección 1
Bloqueada
Recolección para una gran ensalada de frutas 🍎🍊
Recolección para una gran ensalada de frutas 🍎🍊
1
Tarea
JAVA 25 SELF, nivel 32, lección 1
Bloqueada
Diseño del escaparate de objetos cósmicos 🚀
Diseño del escaparate de objetos cósmicos 🚀
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION