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 |
|---|---|---|
|
Dois streams | |
|
Muitos streams, coleção de streams | |
|
Juntar elementos em uma única string | |
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.
GO TO FULL VERSION