List list = names.stream()
    .filter(n -> n.length() > 3)
    .toList();  // Java 16+

Aqui, coletamos os resultados em um conjunto, removendo automaticamente as duplicatas. Use um conjunto quando a exclusividade for mais importante do que a ordem:

Set set = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toSet());

Aqui, coletamos para um Maponde cada chave é o Stringé o comprimento e cada valor é o name em si:

Map map = names.stream()
    .collect(Collectors.toMap(
        String::length,
        n -> n
    ));

Se vários nomes compartilharem o mesmo comprimento, ocorrerá uma colisão. Lide com isso com uma função de mesclagem:

Map safeMap = names.stream()
    .collect(Collectors.toMap(
        String::length,
        n -> n,
        (a, b) -> a   // keep the first value if keys collide
    ));

Unindo cordas

Collectors.joining() mescla todos os elementos do fluxo em um String usando qualquer delimitador que você escolher. Você pode usar “ |”, “ ; ”, ou mesmo“n”para separar valores como quiser:

List names = List.of("Bill", "James", "Patrick");

String result = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.joining(", "));

System.out.println(result);

A saída aqui será: BILL, JAMES, PATRICK.

Agrupando dados

Collectors.groupingBy() agrupa elementos por chave (aqui é o comprimento da string) e retorna um Map>:

List names = List.of("james", "linus", "john", "bill", "patrick");

Map> grouped = names.stream()
    .collect(Collectors.groupingBy(String::length));

A saída será: {4=(john, bill), 5=(james, linus), 7=(patrick)}.

Resumindo números

Você também pode usar coletores para resumir:

List numbers = List.of(3, 5, 7, 2, 10);

IntSummaryStatistics stats = numbers.stream()
    .collect(Collectors.summarizingInt(n -> n));

System.out.println(stats);

A saída neste caso será: IntSummaryStatistics{count=5, sum=27, min=2, average=5.4, max=10}.

Ou, se quiser apenas a média, você poderia fazer:

double avg = numbers.stream()
    .collect(Collectors.averagingDouble(n -> n));

Programação funcional com streams

Anteriormente, mencionei que os fluxos combinam elementos funcionais e declarativos. Vejamos alguns dos elementos de programação funcional em streams.

Lambdas e referências de métodos

Lambdas definem comportamento inline, enquanto referências de método reutilizam métodos existentes:

names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

mapa() vs.

Como regra geral:

  • Use um map() quando você tem uma entrada e deseja uma saída.
  • Use um flatMap() quando você tem uma entrada e deseja muitas saídas (achatadas).

Aqui está um exemplo usando map() em um fluxo:

List> nested = List.of(
    List.of("james", "bill"),
    List.of("patrick")
);

nested.stream()
      .map(list -> list.stream())
      .forEach(System.out::println);

A saída aqui será:

java.util.stream.ReferencePipeline$Head@5ca881b5
java.util.stream.ReferencePipeline$Head@24d46ca6

Existem duas linhas porque existem duas listas internas, então você precisa de duas Stream objetos. Observe também que os valores de hash variam.

Aqui está o mesmo fluxo com flatMap():

nested.stream()
      .flatMap(List::stream)
      .forEach(System.out::println);

Neste caso, a saída será:

 james
 bill
 patrick

Para um aninhamento mais profundo, use:

List>> deep = List.of(
    List.of(List.of("James", "Bill")),
    List.of(List.of("Patrick"))
);

List flattened = deep.stream()
    .flatMap(List::stream)
    .flatMap(List::stream)
    .toList();

System.out.println(flattened);

A saída neste caso será: (James, Bill, Patrick).

Encadeamento opcional

O encadeamento opcional é outra operação útil que você pode combinar com streams:

List names = List.of("James", "Bill", "Patrick");

String found = names.stream()
    .filter(n -> n.length() > 6)
    .findFirst()
    .map(String::toUpperCase)
    .orElse("NOT FOUND");

System.out.println(found);

A saída será: NOT FOUND.

findFirst() retorna um opcionalque representa com segurança um valor que pode não existir. Se nada corresponder, .orElse() fornece um valor substituto. Métodos como findAny(), min()e max() também retorne opcionais pelo mesmo motivo.

Conclusão

A API Java Stream transforma a forma como você lida com dados. Você pode declarar o que deve acontecer — como filtragem, mapeamento ou classificação — enquanto o Java trata com eficiência como isso acontece. A combinação de fluxos, coletores e opcionais torna o Java moderno conciso, expressivo e robusto. Use fluxos para transformar ou analisar coletas de dados, não para tarefas indexadas ou altamente mutáveis. Depois de entrar no fluxo, é difícil voltar aos loops tradicionais.

À medida que você se sentir mais confortável com os conceitos básicos deste artigo, poderá explorar tópicos avançados como fluxos paralelos, fluxos primitivos e coletores personalizados. E não se esqueça de praticar. Depois de entender os exemplos de código aqui, tente executá-los e alterar o código. A experimentação o ajudará a adquirir compreensão e habilidades reais.