1. Introdução
Vamos imaginar que temos uma lista de alunos, e cada aluno tem sua lista de hobbies. Por exemplo:
List<List<String>> hobbies = List.of(
List.of("Natação", "Xadrez"),
List.of("Futebol"),
List.of("Programação", "Leitura", "Cinema")
);
Sua tarefa: obter uma lista única com todos os hobbies, para saber no que os alunos se interessam. Parece lógico tentar usar map:
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
O que obtivemos? Uma lista de fluxos! Ou seja, Stream<Stream<String>>. Não é bem o que precisamos — gostaríamos de obter apenas um Stream<String> para trabalhar diretamente com cada hobby.
Imagine que você tem uma caixa com outras caixas de brinquedos dentro. O método map apenas pega cada caixa e lhe mostra a caixa (Stream<Stream<String>>). Mas você gostaria de ver logo todos os brinquedos (Stream<String>), sem ter que mexer nas caixas.
2. flatMap: como “achatar” coleções aninhadas
Para “desempacotar as caixas” e obter um fluxo plano, é preciso o método flatMap.
Ele recebe uma função que retorna um fluxo para cada elemento e imediatamente “achata” todos os fluxos aninhados em um só.
Sintaxe de flatMap
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Em outras palavras: flatMap espera que você retorne um fluxo (Stream) para cada elemento, e ele vai “achatar” tudo em um único fluxo.
Exemplo: unindo todos os hobbies dos alunos
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Natação, Xadrez, Futebol, Programação, Leitura, Cinema]
Para cada lista de hobbies chamamos list.stream() (obtivemos o fluxo de hobbies de um aluno). E o flatMap “alisou” todos os fluxos em um fluxo único — agora vemos todos os hobbies, e não fluxos de hobbies.
Esquema visual
hobbies.stream()
|
|---> [Natação, Xadrez] -> stream
|---> [Futebol] -> stream
|---> [Programação, ...] -> stream
|
flatMap: combina tudo em um único Stream<String>
Por que map não resolve o problema?
Se usássemos map, obteríamos Stream<Stream<String>>, e trabalhar com isso é inconveniente: por exemplo, não dá para iterar diretamente sobre todas as strings; seriam necessários percursos adicionais.
3. Exemplos práticos de uso de flatMap
Dividindo strings em caracteres (List<String> → Stream<Character>)
Suponha que você tenha uma lista de strings e queira obter um fluxo com todos os caracteres que aparecem em todas as strings:
List<String> words = List.of("Java", "Stream");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
.collect(Collectors.toList());
System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]
Explicação:
- word.chars() retorna um IntStream (fluxo de códigos de caracteres).
- mapToObj transforma os códigos em caracteres.
- flatMap combina todos os fluxos de caracteres em um só.
Trabalhando com Optional: Stream<Optional<T>> → Stream<T>
Suponha que você tenha uma lista de objetos Optional e queira obter um fluxo apenas dos valores que realmente estão presentes:
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Stream")
);
List<String> present = optionals.stream()
.flatMap(opt -> opt.stream())
.collect(Collectors.toList());
System.out.println(present);
// [Java, Stream]
Dica:
Optional<T> (a partir do Java 9) tem o método stream(), que retorna ou um fluxo vazio, ou um fluxo com um único elemento. O flatMap reúne todos os valores não vazios em um único fluxo.
4. O novo método mapMulti: quando flatMap não é necessário
Por que surgiu o mapMulti?
Introduzido no Java 16, o método mapMulti funciona de forma semelhante ao flatMap, mas é um pouco mais eficiente e flexível.
flatMap é uma ferramenta poderosa, mas tem uma desvantagem: para cada elemento você é obrigado a criar um novo Stream, mesmo que queira retornar 0, 1 ou vários elementos. Isso pode ser ineficiente, especialmente se você só deseja “expandir” elementos sem criar coleções ou fluxos intermediários.
mapMulti é uma versão aprimorada do flatMap que permite adicionar valores diretamente ao fluxo resultante via um Consumer, sem criar estruturas intermediárias. Em vez de retornar um fluxo, você indica diretamente quais elementos devem ser adicionados ao fluxo resultante por meio do Consumer.
Sintaxe de mapMulti
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
Para cada elemento, a função mapper é chamada e recebe o próprio elemento e um Consumer no qual você pode “depositar” novos valores.
Exemplo: filtragem e expansão dos elementos em uma única passagem
Suponha que você tenha uma lista de números e queira, para números pares, adicioná-los ao fluxo duas vezes, e para ímpares — nenhuma vez (ou seja, filtragem e “expansão” ao mesmo tempo):
List<Integer> numbers = List.of(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.mapMulti((number, consumer) -> {
if (number % 2 == 0) {
consumer.accept(number);
consumer.accept(number); // Adicionamos duas vezes
}
// Para os ímpares não fazemos nada (filtragem)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
Comparação com flatMap
A mesma tarefa com flatMap ficaria assim:
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
Aqui ainda somos obrigados a criar Stream.of(...) ou Stream.empty() para cada elemento, mesmo que isso seja ineficiente.
5. Quando usar flatMap e quando usar mapMulti?
- map — quando você transforma cada elemento em um único elemento.
- flatMap — quando você transforma cada elemento em um fluxo de elementos.
- mapMulti — quando quer gerar vários elementos sem criar um fluxo intermediário (mais eficiente, especialmente em loops críticos).
Mais um exemplo: desdobrando Map<Integer, List<String>> em Stream<Pair(Integer, String)>
Suponha que temos um mapa: id → lista de hobbies.
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("Natação", "Xadrez"),
2, List.of("Futebol"),
3, List.of("Programação", "Leitura", "Cinema")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Natação, Xadrez, Futebol, Programação, Leitura, Cinema]
E se quisermos obter pares (id, hobby):
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: Natação, 1: Xadrez, 2: Futebol, 3: Programação, 3: Leitura, 3: Cinema]
Com mapMulti (Java 16+):
List<String> pairs = studentHobbies.entrySet().stream()
.mapMulti((entry, consumer) -> {
for (String hobby : entry.getValue()) {
consumer.accept(entry.getKey() + ": " + hobby);
}
})
.collect(Collectors.toList());
Vantagem:
mapMulti não cria fluxos intermediários; ele apenas emite os valores diretamente para o fluxo final.
Visualização: flatMap e mapMulti
| Método | O que a função retorna | Como combina | Coleções intermediárias? |
|---|---|---|---|
|
Um elemento | Simplesmente | Não |
|
Fluxo (Stream) | Concatena | Sim (Streams intermediários) |
|
Consumer (0..n vezes) | Concatena | Não (adiciona diretamente) |
6. Erros comuns ao trabalhar com flatMap e mapMulti
Erro nº 1: Obter Stream<Stream<T>> em vez de Stream<T>. Com frequência, alunos usam map em vez de flatMap quando trabalham com coleções de coleções. Como resultado, acabam tendo que escrever laços extras.
Erro nº 2: Tipo de retorno incorreto. Para flatMap, a função deve retornar um Stream, e não uma List ou um array.
Erro nº 3: Ineficiência. Em casos simples, flatMap funciona muito bem, mas se para cada elemento você precisa criar Stream.of ou Stream.empty(), isso pode ser excessivo. Para tais tarefas é melhor usar mapMulti.
Erro nº 4: mapMulti não funciona em versões antigas do Java. mapMulti apareceu apenas no Java 16. Se você tem uma versão mais antiga do JDK, esse método não estará disponível.
Erro nº 5: Problemas com null. Não retorne null de flatMap — sempre retorne Stream.empty() para casos “vazios”.
Erro nº 6: Coleções intermediárias. Não crie listas ou fluxos extras se puder adicionar elementos diretamente via consumer em mapMulti.
GO TO FULL VERSION