1. O que é processamento preguiçoso?
Processamento preguiçoso, ou “computação preguiçosa” (lazy evaluation), é o princípio em que as operações sobre os dados são adiadas até que o resultado seja realmente necessário. No contexto do Stream API, isso significa: se você escreveu uma cadeia de transformações sobre uma coleção, o Java não as executa imediatamente — ele espera até que uma operação terminal seja chamada. Só então toda a cadeia é processada.
Para que isso serve? Primeiro, economiza recursos: elementos que no fim não serão necessários simplesmente não são processados. Segundo, aumenta a performance — é possível construir cadeias longas sem criar várias coleções intermediárias. Por fim, há operações de “curto-circuito”: assim que o primeiro elemento adequado é encontrado, o processamento é interrompido.
Uma analogia: imagine um garçom preguiçoso. Você diz: “Traga o cardápio, depois um café e depois a sobremesa.” Ele acena com a cabeça, mas não faz nada… até você acrescentar: “Agora, de verdade, traga.” Só então ele vai executar o pedido — e pode trazer apenas o café, se a sobremesa já tiver acabado. É basicamente assim que o processamento preguiçoso funciona nos streams.
2. Operações intermediárias e terminais
Intermediárias (intermediate):
- filter
- map
- sorted
- distinct
- peek (para depuração)
- e outras
As operações intermediárias retornam um novo Stream, mas não iniciam a computação. Elas apenas “montam o plano” de processamento.
Terminais (terminal):
- collect
- forEach
- reduce
- count
- findFirst, findAny
- anyMatch, allMatch, noneMatch
- e outras
Somente uma operação terminal dispara a execução de toda a cadeia.
Exemplo: nada acontece sem uma operação terminal
List<String> names = List.of("Alisa", "Bob", "Vasya");
names.stream()
.filter(name -> {
System.out.println("Filtrando " + name);
return name.startsWith("A");
});
// Nenhuma mensagem será exibida! O código acima apenas "monta" a cadeia.
Agora vamos adicionar uma operação terminal:
names.stream()
.filter(name -> {
System.out.println("Filtrando " + name);
return name.startsWith("A");
})
.forEach(System.out::println);
// Agora veremos a saída no console!
Resultado:
Filtrando Alisa
Filtrando Bob
Filtrando Vasya
Alisa
3. Vantagens do processamento preguiçoso
Economia de recursos
O processamento preguiçoso permite não gastar tempo e memória com elementos desnecessários. Por exemplo, se você procura o primeiro objeto adequado, o processamento será interrompido no primeiro acerto.
List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");
String firstA = names.stream()
.filter(name -> {
System.out.println("Verificando: " + name);
return name.startsWith("A");
})
.findFirst()
.orElse("Não encontrado");
System.out.println("Resultado: " + firstA);
Saída:
Verificando: Alisa
Resultado: Alisa
Atenção: os demais elementos nem são verificados!
Cadeias longas sem coleções intermediárias
É possível combinar muitas operações (filter, map, sorted etc.) sem criar coleções em cada etapa.
List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList(); // Java 16+, antes — .collect(Collectors.toList())
Curto-circuito
Se basta saber “existe algum elemento adequado?”, os demais não serão verificados:
boolean hasLongName = names.stream()
.anyMatch(name -> {
System.out.println("Verificando: " + name);
return name.length() > 10;
});
// Se o primeiro elemento for longo — os demais não serão verificados!
4. Exemplos: como o processamento preguiçoso funciona
Exemplo 1: nada acontece sem uma operação terminal
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream()
.filter(n -> {
System.out.println("Filtrando " + n);
return n % 2 == 0;
});
// Sem saída!
Exemplo 2: cadeia com operação terminal
numbers.stream()
.filter(n -> {
System.out.println("Filtrando " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("Multiplicando " + n);
return n * 10;
})
.forEach(System.out::println);
Saída:
Filtrando 1
Filtrando 2
Multiplicando 2
20
Filtrando 3
Filtrando 4
Multiplicando 4
40
Filtrando 5
Observação importante: as operações são executadas elemento a elemento: primeiro filter, depois map, depois forEach — para cada elemento em sequência. Não são duas passagens separadas “primeiro filtrar tudo, depois transformar tudo”.
Exemplo 3: usando peek para depuração
numbers.stream()
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("Passou no filtro: " + n))
.map(n -> n * 10)
.peek(n -> System.out.println("Após map: " + n))
.forEach(System.out::println);
5. Nuances úteis
Não use streams com efeitos colaterais
A preguiça pode pregar uma peça se você contar com execução imediata. Ações com efeitos colaterais dentro de map, filter ou peek (escrita em arquivo, alteração de estado externo) podem ocorrer em ordem diferente, não para todos os elementos ou nem ocorrer sem uma operação terminal.
Filtre o mais cedo possível
Coloque filter perto do início da cadeia, para descartar elementos supérfluos antes e reduzir o volume do trabalho subsequente.
Precisa apenas do primeiro resultado? Use os terminais adequados
Se você precisa do primeiro elemento adequado, chame findFirst ou findAny. Isso permite que o stream pare assim que o resultado for encontrado.
Streams não servem para alterar a coleção de origem
Streams não são destinados a adicionar/remover elementos da coleção original. Para modificar a estrutura da coleção, use outros mecanismos.
Visualização do funcionamento de streams preguiçosos
List<String> words = List.of("cat", "dog", "elephant", "fox", "giraffe");
words.stream()
.filter(w -> w.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println);
Como isso acontece:
| Etapa | cat | dog | elephant | fox | giraffe |
|---|---|---|---|---|---|
|
✗ | ✗ | ✓ | ✗ | ✓ |
|
— | — | ELEPHANT | — | GIRAFFE |
|
— | — | impressão | — | impressão |
Tabela: comparação entre as abordagens eager e lazy
| Abordagem | Quando o processamento é executado? | Uso de memória | Desempenho |
|---|---|---|---|
| Eager (ávido) | Imediatamente na chamada | Pode ser alto | Às vezes lento |
| Lazy (preguiçoso) | Somente quando necessário | Mínimo | Geralmente mais rápido |
Abordagem ávida — por exemplo, quando você organiza manualmente várias passagens pela coleção, criando listas intermediárias.
Abordagem preguiçosa — são os streams: nada é feito até que o resultado final seja necessário.
6. Erros comuns ao trabalhar com streams preguiçosos
Erro nº 1: expectativa de resultado imediato. Iniciantes pensam que chamadas a filter ou map são executadas na hora. Mas sem um terminal (por exemplo, collect, forEach) nada acontece — daí “a depuração não funciona”, “nada é impresso”.
Erro nº 2: efeitos colaterais em operações intermediárias. Escrita em arquivo, alteração de variáveis externas dentro de map/filter/peek — má prática. Devido à preguiça e otimizações, tais ações podem não ser executadas completamente, não na ordem esperada ou nem ser executadas sem uma operação terminal.
Erro nº 3: esqueceu de chamar uma operação terminal. Uma cadeia de streams foi escrita, mas não há encerramento por collect, forEach etc. Resultado — “silêncio”.
Erro nº 4: esperar que todos os elementos sejam processados. Operações como findFirst ou anyMatch interrompem o pipeline no primeiro resultado. Os demais elementos não são processados — daí a surpresa “por que meu println não rodou para todos?”.
Erro nº 5: usar streams para alterar a coleção original. Streams não foram feitos para modificar as coleções de origem (adicionar/remover elementos). Use métodos especializados das coleções ou iteradores.
GO TO FULL VERSION