CodeGym /Cursos /JAVA 25 SELF /Métodos forEach, peek: efeitos colaterais

Métodos forEach, peek: efeitos colaterais

JAVA 25 SELF
Nível 30 , Lição 3
Disponível

1. Método forEach: ação final sobre os elementos

Antes de mergulharmos nos detalhes, vamos relembrar o que são operações terminais e intermediárias de um fluxo.

  • Operações intermediárias (por exemplo, filter, map, distinct, peek) — retornam um novo fluxo e geralmente não executam ações até que uma operação terminal seja chamada.
  • Operações terminais (por exemplo, forEach, collect, count, anyMatch) — disparam o processamento dos elementos do fluxo e retornam um resultado (ou não retornam nada, como forEach).

Depois de uma operação terminal, o fluxo é considerado fechado, e não é mais possível aplicar outras operações a ele. É como tentar terminar um sorvete que já foi comido — não vai dar: o fluxo já foi “usado”.

Agora, vamos conhecer dois métodos importantes para lidar com efeitos colaterais: forEach e peek.

O que o forEach faz?

forEach é uma operação terminal do fluxo que executa a ação fornecida para cada elemento do fluxo. Geralmente é usada para imprimir na tela, registrar em log, coletar estatísticas e outros efeitos colaterais (side effects).

Assinatura do método:

void forEach(Consumer<? super T> action)

Consumer<T> é uma interface funcional que recebe um argumento e não retorna nada (por exemplo, System.out::println).

Exemplo: imprimir todos os elementos da lista

Suponha que temos uma lista de nomes de usuários:

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

Imprimir todos os usuários na tela com a Stream API é muito simples:

users.stream().forEach(System.out::println);

Resultado:

Anna
Boris
Alex
Alina
Dmitry

Também é possível usar uma expressão lambda:

users.stream().forEach(name -> System.out.println("Usuário: " + name));

Resultado:

Usuário: Anna
Usuário: Boris
Usuário: Alex
Usuário: Alina
Usuário: Dmitry

Importante: forEach encerra o fluxo

Após chamar forEach, o fluxo “se encerra”. Não é possível continuar a cadeia:

users.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println)
     .map(String::toUpperCase); // Erro! O fluxo já foi encerrado.

Tentar chamar algo depois de forEach levará a um erro de compilação: a operação terminal retorna void, e não um novo fluxo.

2. Método peek: espiamos, mas não interferimos

peek é uma operação intermediária. Ela permite executar uma ação para cada elemento em um determinado estágio do processamento, sem alterar o próprio elemento e sem encerrar o fluxo.

Assinatura do método:

Stream<T> peek(Consumer<? super T> action)
  • peek retorna um novo fluxo no qual a ação action será executada para cada elemento.
  • Normalmente é usada para depuração, registro em log ou monitoramento do estado do fluxo.

Exemplo: log após filtragem

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

List<Integer> nameLengths = users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Passou pelo filtro: " + name))
    .map(String::length)
    .collect(Collectors.toList());

Resultado no console:

Passou pelo filtro: Anna
Passou pelo filtro: Alex
Passou pelo filtro: Alina

Conteúdo de nameLengths:

[4, 4, 5]

Onde é conveniente usar peek?

  • Para depurar a cadeia de operações: ver o que acontece em cada etapa.
  • Para coleta/contagem de estatísticas (por exemplo, contabilizar a quantidade de elementos).
  • Para registrar dados em log em etapas intermediárias.

Importante: peek não deve ser usado para alterar os elementos do fluxo. Para transformações existe map. peek é “espiar”, não “interferir”.

3. forEach vs peek: qual é a diferença?

Método Tipo de operação Quando se aplica É possível continuar a cadeia? Para que é mais adequado
forEach
Terminal No fim do processamento do fluxo Não Ações finais (saída, registro em log, gravação no BD)
peek
Intermediária No meio da cadeia de operações Sim Depuração, registro em log intermediário, contagem

Exemplo: diferença no uso

// Exemplo com forEach
users.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println); // Aqui o fluxo é encerrado

// Exemplo com peek
users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Passou pelo filtro: " + name))
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // É possível continuar a cadeia

É importante lembrar

  • forEach é o ponto sem retorno: depois dele não há mais nada a fazer com o fluxo.
  • peek não garante a execução das ações se nenhuma operação terminal for chamada. Se você escrever apenas uma cadeia de operações intermediárias — nada acontecerá.

4. Aspectos não óbvios: forEach nem sempre é a melhor escolha!

Por que não usar forEach para modificar coleções?

Muitos iniciantes tentam usar forEach para alterar elementos da coleção ou a própria coleção (por exemplo, remover elementos). Mas essa é uma prática ruim: streams não foram feitos para modificar coleções de origem.

Exemplo de uso incorreto:

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // Pode levar a ConcurrentModificationException!
    }
});

Resultado: erro em tempo de execução — não é possível modificar a coleção durante a iteração do fluxo (ConcurrentModificationException).

Afinal, para que usar forEach?

  • Para imprimir na tela (por exemplo, imprimir um relatório).
  • Para registro em log.
  • Para chamar serviços externos (por exemplo, envio de e-mail).
  • Para coletar estatísticas (por exemplo, incremento de contador).

5. Mais uma vez sobre peek: apenas para depuração!

É tentador usar peek para alterar elementos, por exemplo, aumentar a idade do usuário:

users.stream()
    .peek(user -> user.setAge(user.getAge() + 1)) // Ruim!
    .collect(Collectors.toList());

Por que isso é ruim?

  • Viola a declaratividade e a pureza da Stream API.
  • Esse código fica difícil de manter e testar.
  • Efeitos colaterais em uma operação intermediária podem levar a bugs não óbvios.

É melhor usar map para transformar os dados:

List<User> olderUsers = users.stream()
    .map(user -> new User(user.getName(), user.getAge() + 1))
    .collect(Collectors.toList());

Esquema: diferença entre forEach e peek

users.stream()
    .filter(...)         // operação intermediária
    .peek(...)           // operação intermediária, "espiamos"
    .map(...)            // operação intermediária
    .forEach(...)        // operação terminal, "fazemos a ação"

Explicação:
— Tudo o que vem antes de forEach pode ser combinado, reordenado, acrescentado.
— Após forEach, o fluxo está encerrado.

6. Erros típicos ao trabalhar com forEach e peek

Erro nº 1: Usar peek para modificar dados. peek é destinado apenas à observação, não à modificação dos elementos do fluxo. Para transformações, use map.

Erro nº 2: Esperar que peek sempre seja executado. peek só é executado se após ele houver uma operação terminal (collect, forEach, count etc.). Sem uma operação terminal, nada acontecerá.

Erro nº 3: Tentar continuar o fluxo após forEach. forEach é uma operação terminal. Depois dela, não é possível chamar outros métodos do fluxo.

Erro nº 4: Modificar a coleção dentro de forEach. Alterar a coleção de origem (remover ou adicionar elementos) durante a iteração via forEach é caminho certo para ConcurrentModificationException.

Erro nº 5: Usar forEach em vez de collect para reunir o resultado. Se você deseja reunir elementos em uma nova coleção, use collect(Collectors.toList()), e não forEach com adição manual. Isso fere a declaratividade e pode levar a erros em cenários multithread.

1
Tarefa
JAVA 25 SELF, nível 30, lição 3
Bloqueado
Chamada dos Alunos ao Quadro 👩‍🏫
Chamada dos Alunos ao Quadro 👩‍🏫
1
Tarefa
JAVA 25 SELF, nível 30, lição 3
Bloqueado
Depuração do Fluxo de Dados na Casa Inteligente 🏠
Depuração do Fluxo de Dados na Casa Inteligente 🏠
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION