CodeGym /Cursos /JAVA 25 SELF /Métodos join e concat: união de streams

Métodos join e concat: união de streams

JAVA 25 SELF
Nível 32 , Lição 1
Disponível

1. Stream.concat: unindo dois streams

Na programação, muitas vezes surge a situação: temos dois (ou mais) streams de dados e queremos uni-los em um só. Por exemplo, duas listas de estudantes de turmas diferentes — e precisamos processar todos de uma vez. Antigamente (antes do Stream API), simplesmente uniríamos duas listas com addAll. Mas, se estamos trabalhando com streams (Stream<T>), queremos fazer isso de forma lazy e expressiva.

Sintaxe e princípio de funcionamento

A maneira mais básica de unir dois streams é usar o método estático Stream.concat:

Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)

O que acontece:

  • Primeiro vem o primeiro stream, depois o segundo.
  • A saída é um novo stream, que primeiro entrega todos os elementos do primeiro e depois todos do segundo.
  • A união é lazy: enquanto você não começar a consumir o stream resultante com uma operação terminal (por exemplo, forEach ou collect), nada acontece.

Exemplo: unindo duas listas de nomes

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: após a união, o stream resultante é de uso único. Como qualquer outro Stream, ele não pode ser reutilizado.

Particularidades de Stream.concat

  • Apenas dois streams por vez. Para três ou mais — encadeie concat ou use outras abordagens (veja abaixo).
  • A ordem é preservada. Primeiro os elementos do primeiro stream, depois do segundo.
  • União lazy. Se o primeiro stream for infinito, a execução nunca chegará ao segundo.
  • Bug típico: tentar unir um stream com ele mesmo (Stream.concat(stream, stream)) leva à reutilização do mesmo stream — isso é proibido.

2. Unindo mais de dois streams: flatMap e Stream.of

Quando há mais de dois streams, é mais prático juntá-los com a combinação Stream.of + flatMap.

Usando 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);

O que acontece aqui:

  • Stream.of(groupA, groupB, groupC) cria um stream de três coleções.
  • flatMap(Collection::stream) desdobra cada coleção em um único stream de strings.
  • O resultado é um único stream com todos os estudantes.

Vantagem: é possível unir quantos streams forem necessários.

Outra opção: unir uma 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: unindo strings com separador

Às vezes, não basta apenas juntar streams, mas sim colar todos os elementos em uma única string — por exemplo, para imprimir nomes separados por vírgula. Para isso, existe o collector Collectors.joining.

Sintaxe

String result = stream.collect(Collectors.joining(", "));
  • Sem argumentos: cola as strings sem separador.
  • Com separador: entre os elementos haverá a string informada.
  • Com prefixo e sufixo: Collectors.joining(delimiter, prefix, suffix)

Exemplo: lista de estudantes em uma única string

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 estudantes: " + line);

Resultado:

Lista de estudantes: Anya, Boris, Vika

Exemplo com prefixo e sufixo

String line = students.stream()
    .collect(Collectors.joining(", ", "[", "]"));

System.out.println(line);

Resultado:

[Anya, Boris, Vika]

Para que isso serve?

  • Geração de linhas CSV.
  • Impressão elegante de listas e relatórios.
  • Envio de dados em uma única string (por exemplo, em URL ou log).

4. Comparação concat vs flatMap: quando usar o quê?

  • Stream.concat — quando há exatamente dois streams e é importante preservar a ordem.
  • Stream.of + flatMap — quando há muitos streams ou eles estão armazenados em uma coleção.
  • Collectors.joining — quando os elementos são do tipo String e o resultado precisa ser uma única string.

Tabela de comparação

Método Quando usar Exemplo de código
Stream.concat(a, b)
Dois streams
Stream.concat(a, b)
Stream.of(...).flatMap()
Muitos streams, coleção de streams
Stream.of(a, b, c).flatMap(x -> x)
Collectors.joining()
Juntar elementos em uma única string
stream.collect(Collectors.joining(", "))

5. Exemplos práticos em um app educativo

Exemplo 1. Unindo estudantes de duas faculdades

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);

Exemplo 2. Unindo estudantes de todas as turmas e imprimindo em uma única string

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 os estudantes: " + allNames);

Resultado:

Todos os estudantes: Anya; Boris; Vika; Grisha; Dasha

Exemplo 3. Unindo 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. Nuances e particularidades importantes

concat funciona apenas com dois streams

Para três streams, você pode escrever de forma aninhada — funciona, mas fica verboso. É melhor 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);

Streams são de uso único

Depois de uma operação terminal, o stream não pode ser usado novamente. Reutilizá-lo resulta em IllegalStateException.

O concat é lazy

Se o primeiro stream for infinito (por exemplo, criado via Stream.generate(...)), a execução nunca chegará ao segundo.

Ordem dos elementos

A ordem é sempre preservada: primeiro o primeiro stream, depois o segundo.

Collectors.joining funciona apenas com Stream<String>

Para tipos não-string, primeiro converta os elementos para string, por exemplo, com 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

Comparação com a abordagem “manual”

Antes do Stream API:

import java.util.ArrayList;
import java.util.List;

List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);

Com o Stream API:

import java.util.stream.Collectors;

List<String> merged = Stream.concat(listA.stream(), listB.stream())
    .collect(Collectors.toList());

Vantagem dos streams: é possível encadear operações intermediárias (filter, map, etc.) antes e depois da união, além de trabalhar com diversas fontes de dados.

7. Erros comuns ao unir streams

Erro nº 1: reutilização do stream. Um stream só pode ser consumido uma vez. Não é possível uni-lo consigo mesmo.

import java.util.stream.Stream;

Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // Erro! s será reutilizado

Erro nº 2: tentar unir um stream infinito com um finito. Se o primeiro stream for infinito, o segundo nunca começará.

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 não entrará no resultado

Erro nº 3: joining para não-strings. O collector Collectors.joining aceita um stream de strings. Para números, é preciso converter antes para strings:

import java.util.List;
import java.util.stream.Collectors;

List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // Erro de compilação!

String joined = numbers.stream()
    .map(Object::toString)
    .collect(Collectors.joining(", "));

Erro nº 4: quebra de ordem. Se a ordem for importante, use concat ou um flatMap ordenado. Métodos como unordered() podem quebrar a ordem determinística da saída.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION