CodeGym /Cursos /JAVA 25 SELF /Processamento preguiçoso (lazy evaluation) no Stream API

Processamento preguiçoso (lazy evaluation) no Stream API

JAVA 25 SELF
Nível 33 , Lição 1
Disponível

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
filter
map
ELEPHANT GIRAFFE
forEach
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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION