Pipelines em vez de laços
Um stream é um pipeline para processar uma sequência de valores. Você parte de uma fonte - normalmente uma coleção - encadeia operações como "mantenha apenas estes", "transforme cada um", "ordene-os" e termina coletando o resultado. Em vez de escrever um laço com uma lista temporária e um if dentro, você descreve o que quer e deixa o stream percorrer tudo.
Agora que você tem lambdas, os streams se encaixam: cada operação recebe uma lambda (ou uma referência a método) que descreve o trabalho para um único elemento.
Leia de cima para baixo: pegue os nomes, mantenha os longos, deixe em maiúsculas, colete em uma lista. A lista original names nunca é modificada - um stream produz um novo resultado e deixa a fonte intacta.
A anatomia de um stream
Todo pipeline tem três partes:
- Uma fonte -
list.stream(),Arrays.stream(array)ouStream.of(a, b, c). - Zero ou mais operações intermediárias -
filter,map,sorted,distinct,limit. Cada uma retorna outro stream, então você pode encadeá-las. - Exatamente uma operação terminal -
collect,count,forEach,reduce,findFirst. Ela dispara o trabalho e produz um resultado (ou um efeito colateral).
Essa divisão importa por causa de uma regra que surpreende todo mundo no início.
Operações intermediárias são preguiçosas
As operações intermediárias não fazem nada por conta própria. Elas apenas registram o que deveria acontecer. O pipeline só roda quando uma operação terminal pede um resultado.
Execute: a lambda do map nunca roda. Adicione uma operação terminal e toda a cadeia ganha vida:
Essa preguiça não é uma esquisitice para combater - ela permite que o stream funda operações e pule o trabalho de que não precisa. Mas significa que um pipeline sem operação terminal não faz nada, o que é um bug comum do tipo "por que nada está acontecendo?".
filter e map: manter e transformar
Esses dois carregam quase todo o peso. filter recebe um Predicate (uma lambda que retorna boolean) e mantém os elementos que passam. map recebe uma Function e substitui cada elemento pelo resultado de aplicá-la.
Note que map pode mudar o tipo do elemento: aqui um stream de String se torna um stream de Integer. O String::length é uma referência a método - uma forma abreviada da lambda w -> w.length().
Operações terminais produzem um resultado
Depois de moldar o stream, uma operação terminal o transforma em algo concreto:
Operações terminais comuns: collect (reunir em uma lista/conjunto/mapa), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max e reduce. Depois que uma terminal roda, o stream fica consumido - você não pode reutilizá-lo. Chame list.stream() de novo para obter um pipeline novo.
Coletando resultados
collect com Collectors é o burro de carga para construir um resultado. O mais comum é Collectors.toList():
Collectors também oferece toSet(), joining(...), groupingBy(...) e counting(). No Java 16 e posteriores você pode substituir collect(Collectors.toList()) pelo mais curto .toList() (ele retorna uma lista imutável):
List<String> result = names.stream().map(String::toUpperCase).toList();
sorted, distinct e limit
Essas operações intermediárias remodelam o stream antes de você coletar:
sorted() sem argumento usa a ordenação natural; passe um Comparator (aqui uma lambda) para uma ordem personalizada. Comparator.reverseOrder() é a forma mais limpa de ordenar em ordem decrescente.
reduce: dobrar um stream em um único valor
Quando você precisa combinar todos os elementos em um único resultado - uma soma, um produto, a string mais longa - reduce é a ferramenta geral. Você fornece um valor inicial e uma função que mescla dois valores:
Para somas e médias simples, os streams especializados são mais claros: nums.stream().mapToInt(Integer::intValue).sum(). Recorra a reduce quando não houver um agregador pronto.
Streams não substituem todo laço
Streams brilham quando você está transformando uma coleção em um resultado. Eles não são automaticamente mais rápidos que um laço, e ficam desajeitados quando você precisa alterar estado externo ou sair antecipadamente de formas complicadas. Uma boa regra: se o pipeline se lê como uma frase clara, use um stream; se você está recorrendo a um contador compartilhado ou a um index, um laço for comum é honesto e está ótimo.
Lembre-se também de que um stream é de uso único. Isto lança uma exceção:
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count(); // IllegalStateException: stream has already been operated upon
Próximo: Optional
Várias operações terminais dos streams - findFirst, min, max, reduce sem identidade - podem não encontrar nada, então não retornam um valor cru. Elas retornam um Optional, o contêiner do Java para "talvez um valor, talvez nada", que finalmente oferece uma alternativa limpa a retornar null. Essa é a próxima página.
Perguntas frequentes
O que é um stream em Java?
Um stream é um pipeline para processar uma sequência de elementos - normalmente vindos de uma coleção - por meio de uma cadeia de operações como filter, map e sorted, terminando em uma operação terminal como collect ou count. Ele não armazena dados e não modifica a fonte; ele descreve um cálculo. Você cria um com list.stream() e lê a cadeia de cima para baixo, como uma receita.
Como converter um stream de volta em uma lista em Java?
Finalize o pipeline com um coletor terminal: list.stream().filter(...).collect(Collectors.toList()). No Java 16+ você pode usar o mais curto .toList(), que retorna uma lista imutável. Sem uma operação terminal nada é executado, porque operações intermediárias como filter e map são preguiçosas.
Quando usar um stream em vez de um laço for?
Use um stream quando estiver transformando ou filtrando uma coleção em um resultado - o pipeline se lê como uma frase clara ("pegue os nomes, mantenha os longos, deixe em maiúsculas, colete em uma lista"). Fique com um laço for comum quando precisar alterar estado externo, sair antecipadamente de formas complexas, ou quando a lógica for mais simples como passos imperativos. Streams são sobre clareza, não sobre velocidade pura.