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 |
|---|---|---|---|---|
|
Terminal | No fim do processamento do fluxo | Não | Ações finais (saída, registro em log, gravação no BD) |
|
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.
GO TO FULL VERSION