CodeGym /Cursos /JAVA 25 SELF /Operações básicas da Stream API: map, filter, collect

Operações básicas da Stream API: map, filter, collect

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

1. Criando um stream

Para usar a Stream API, primeiro é preciso obter um stream de alguma coleção ou array.

Exemplos de criação de stream

// De uma lista
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();

// De um array
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);

// De valores individuais
Stream<String> letters = Stream.of("A", "B", "C");

Em resumo:

  • list.stream() — para coleções
  • Arrays.stream(array) — para arrays
  • Stream.of(...) — para valores individuais

Exemplo no contexto do nosso aplicativo

Suponha que temos uma lista de usuários:

List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();

Operações intermediárias e terminais

Ponto importante: as operações na Stream API se dividem em dois tipos.

  • Operações intermediárias (por exemplo, filter, map, distinct) — descrevem as etapas de processamento. Elas retornam um novo stream, mas por si só não disparam nada.
  • Operações terminais (por exemplo, collect, forEach, count) — disparam o pipeline e produzem um resultado.

O stream funciona de forma “preguiçosa”: enquanto uma operação terminal não for chamada, nenhum cálculo será feito. É por isso que muitas vezes terminamos a cadeia com collect(...) — esse é o ponto em que o stream é convertido de volta em uma coleção ou em outro resultado.

2. Operação filter: filtrando elementos por uma condição

filter é uma operação intermediária que deixa passar apenas os elementos que atendem à condição especificada.

Assinatura

Stream<T> filter(Predicate<? super T> predicate); 

Predicate é uma interface funcional que recebe um elemento e retorna true (manter) ou false (filtrar fora).

Exemplo: manter apenas os números pares

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

O que acontece?

  • n -> n % 2 == 0 — uma expressão lambda que verifica se o número é divisível por 2 sem resto.
  • filter mantém apenas os números pares.

Exemplo: filtrar nomes que começam com "A"

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

List<String> aNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

System.out.println(aNames); // [Anna, Alex, Alina]

Ponto importante: filter não modifica a coleção — ele cria um novo stream contendo apenas os elementos necessários.

3. Operação map: transformando um elemento em outra coisa

map é uma operação de transformação. Ela pega cada elemento do stream, aplica uma função e retorna um novo elemento.

Assinatura

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Function é uma interface que recebe um elemento e retorna algo (podendo ser de outro tipo).

Exemplo: obter os comprimentos das strings

List<String> names = List.of("Anna", "Boris", "Alex");

List<Integer> nameLengths = names.stream()
    .map(name -> name.length())
    .collect(Collectors.toList());

System.out.println(nameLengths); // [4, 5, 4]

O que acontece?

  • map transforma a string em seu comprimento (name -> name.length()).
  • Como resultado, obtemos um stream de números.

Exemplo: converter as strings para maiúsculas

List<String> names = List.of("Anna", "Boris", "Alex");

List<String> upperNames = names.stream()
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

System.out.println(upperNames); // [ANNA, BORIS, ALEX]

4. Operação collect: recolhendo o resultado de volta em uma coleção

collect é uma operação terminal, ou seja, ela finaliza o trabalho do stream e reúne o resultado em uma coleção ou outro contêiner.

Assinatura

<R, A> R collect(Collector<? super T, A, R> collector)

Não se assuste com a assinatura assustadora! Em 99 % dos casos você usa coletores prontos da classe Collectors.

Collectors é uma classe utilitária com um conjunto de “coletores”. Ela diz ao stream em que forma reunir o resultado: lista, conjunto, string etc.

Exemplos:

  • Collectors.toList() — em List
  • Collectors.toSet() — em Set
  • Collectors.joining(", ") — em uma string separada por vírgula

Ou seja, Collectors é como um conjunto de caixas de diferentes formatos nas quais você empacota os elementos do stream.

Exemplo: reunir o resultado em um List

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .collect(Collectors.toList());

Exemplo: reunir o resultado em um Set

Set<String> uniqueNames = names.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

Exemplo: reunir strings separadas por vírgula

String result = names.stream()
    .collect(Collectors.joining(", "));

System.out.println(result); // Anna, Boris, Alex

5. Cadeia de operações: filtragem + transformação + coleta do resultado

A maior força da Stream API é a capacidade de encadear operações uma após a outra.

Exemplo: obter os comprimentos dos nomes que começam com "A"

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

List<Integer> aNameLengths = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::length)
    .collect(Collectors.toList());

System.out.println(aNameLengths); // [4, 4, 5]

Passo a passo:

  1. .stream() — criamos um stream a partir da lista.
  2. .filter(name -> name.startsWith("A")) — mantemos apenas os nomes que começam com "A".
  3. .map(String::length) — transformamos cada nome em seu comprimento.
  4. .collect(Collectors.toList()) — reunimos o resultado em uma lista.

Código imperativo equivalente

Veja como ficaria o mesmo “à moda antiga”:

List<Integer> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.length());
    }
}

Compare: com a Stream API — uma linha, lê-se “o que fazemos”, e não “como fazemos”.

6. Prática: algumas tarefas curtas

Vamos praticar! Todos os exemplos podem ser executados em um único arquivo — basta mudar os dados.

Tarefa 1: manter apenas os números ímpares e elevá-los ao quadrado

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

List<Integer> oddSquares = numbers.stream()
    .filter(n -> n % 2 != 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

System.out.println(oddSquares); // [1, 9, 25, 49]

Tarefa 2: a partir de uma lista de strings, obter a lista de suas primeiras letras

List<String> names = List.of("Anna", "Boris", "Alex");

List<Character> initials = names.stream()
    .map(name -> name.charAt(0))
    .collect(Collectors.toList());

System.out.println(initials); // [A, B, A]

Tarefa 3: filtrar strings com comprimento maior que 3 e reuni-las em um Set

List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");

Set<String> longWords = words.stream()
    .filter(word -> word.length() > 3)
    .collect(Collectors.toSet());

System.out.println(longWords); // [bear, elephant]

7. Erros típicos ao trabalhar com filter, map, collect

Erro nº 1: esqueceu do collect — nenhum resultado!
A Stream API é preguiçosa, como um gato no parapeito da janela: enquanto você não chamar uma operação terminal (por exemplo, collect ou forEach), nada vai acontecer. Se você escrever apenas users.stream().filter(...).map(...); — isso não executará nenhuma ação.

Erro nº 2: filter e map invertidos
Às vezes iniciantes fazem map primeiro, e depois filter. Por exemplo, names.stream().map(String::length).filter(len -> len > 3) — isso produzirá números, não strings. Se você precisa de strings com comprimento maior que 3, primeiro filtre, depois transforme.

Erro nº 3: esqueceu a imutabilidade
As operações da Stream API não alteram a coleção original! Elas retornam um novo resultado. Após List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — a coleção names permanecerá a mesma.

Erro nº 4: tentar usar uma lista externa mutável
Não faça assim:

List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));

Prefira usar collect — é mais seguro e mais curto.

Erro nº 5: NullPointerException
Se a coleção puder conter elementos null, a chamada name.startsWith("A") em null causará um erro. Adicione um filtro para null, se for possível:

.filter(name -> name != null && name.startsWith("A"))
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION