CodeGym /Cursos /JAVA 25 SELF /Métodos reduce e collect: agregação de dados

Métodos reduce e collect: agregação de dados

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

1. Método reduce: redução universal

Na programação, muitas vezes é necessário “reduzir” uma coleção a um único valor final: somar, encontrar o produto, concatenar strings, calcular uma métrica agregada ou reunir elementos em uma nova estrutura. Antes, isso era feito manualmente com laços e uma variável acumuladora. Hoje, a API de Stream oferece maneiras elegantes — os métodos universais reduce e collect, que permitem escrever código de forma compacta e declarativa.

  • reduce — reduz o stream a um único valor final (soma, produto, concatenação etc.).
  • collect — transforma o stream em uma coleção, string, mapa ou estrutura arbitrária.

Vamos por partes.

Para que serve reduce?

reduce é um método terminal que “reduz” os elementos do stream a um único valor usando uma função acumuladora. Mentalmente, isso parece uma passagem pela coleção com acumulação passo a passo do resultado.

Assinaturas do método reduce

Na API de Stream há três variantes principais de reduce():

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  • accumulator — função que recebe o valor acumulado atual e o próximo elemento, e retorna o novo resultado.
  • identity — valor inicial do acumulador (por exemplo, 0 para soma, 1 para produto).
  • combiner — usado em streams paralelos para unir resultados intermediários.

Exemplos de uso de reduce

Exemplo 1: Soma de números

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

// reduce sem identity — o resultado é Optional
Optional<Integer> sum1 = numbers.stream()
    .reduce((a, b) -> a + b);

System.out.println(sum1.orElse(0)); // 15

// reduce com identity — o resultado sempre existe
int sum2 = numbers.stream()
    .reduce(0, (a, b) -> a + b);

System.out.println(sum2); // 15

Exemplo 2: Produto de todos os números

int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);
System.out.println(product); // 120

Exemplo 3: Concatenação de strings

List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
    .reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API

Exemplo 4: Busca do elemento máximo

Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);
max.ifPresent(System.out::println); // 5

Exemplo 5: Soma dos comprimentos de todas as strings

List<String> texts = List.of("gato", "cachorro", "elefante");
int totalLength = texts.stream()
    .map(String::length)
    .reduce(0, Integer::sum);
System.out.println(totalLength); // 20

Como reduce funciona

A lógica de reduce é equivalente ao seguinte laço:

T result = identity;
for (T element : collection) {
    result = accumulator.apply(result, element);
}
return result;

Se identity não for definido, o valor inicial passa a ser o primeiro elemento do stream, e o método retorna um Optional (pode estar vazio se o stream estiver vazio).

2. Método collect: transformação universal

collect é um método terminal que transforma o stream em uma coleção, string, mapa ou qualquer outra estrutura. Para isso, são usados “coletores” (Collector) que descrevem o processo de montagem. Na maioria das vezes, usamos os prontos da classe Collectors.

Coletores mais populares

  • Collectors.toList() — coleta os elementos em uma List.
  • Collectors.toSet() — coleta os elementos em um Set.
  • Collectors.toMap() — coleta os elementos em um Map.
  • Collectors.joining() — concatena strings em uma só.
  • Collectors.groupingBy() — agrupa elementos por um critério.
  • Collectors.counting() — conta a quantidade de elementos.
  • Collectors.summarizingInt() — reúne estatísticas sobre números (soma, média, min/máx).

Exemplos de uso de collect

Exemplo 1: Coleta em lista

List<String> names = List.of("Anya", "Boris", "Vasya", "Anya");
List<String> uniqueNames = names.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(uniqueNames); // [Anya, Boris, Vasya]

Exemplo 2: Coleta em conjunto (Set)

Set<String> nameSet = names.stream()
    .collect(Collectors.toSet());
System.out.println(nameSet); // [Anya, Boris, Vasya] (a ordem não é garantida)

Exemplo 3: Coleta em string

String csv = names.stream()
    .collect(Collectors.joining(", "));
System.out.println(csv); // Anya, Boris, Vasya, Anya

Exemplo 4: Coleta em Map

Suponha que temos a classe:

public class Employee {
    private String name;
    private String department;
    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
    }
    public String getName() { return name; }
    public String getDepartment() { return department; }
}

Vamos montar um mapa “nome → departamento”:

List<Employee> employees = List.of(
    new Employee("Anya", "IT"),
    new Employee("Boris", "HR"),
    new Employee("Vasya", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // tratamento de nomes duplicados
    ));

System.out.println(nameToDept); // {Anya=IT, Boris=HR, Vasya=IT}

Exemplo 5: Coletar elementos únicos em Set

Set<String> unique = names.stream()
    .collect(Collectors.toSet());
System.out.println(unique);

Exemplo 6: Coletar estatísticas de números

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
    .collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum());     // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax());     // 5
System.out.println(stats.getMin());     // 1

3. Comparação: quando usar reduce e quando usar collect

reduce — quando é preciso obter um único valor final com uma operação binária: soma, produto, máximo, concatenação.

collect — quando é necessário reunir elementos em uma coleção/mapa/string ou realizar uma agregação mais complexa com Collector. Para essas tarefas, collect costuma ser mais poderoso e eficiente.

Tabela: reduce vs collect

Tarefa O que usar Exemplo
Soma de números
reduce
reduce(0, Integer::sum)
Produto
reduce
reduce(1, (a, b) -> a * b)
Coletar em List
collect
collect(Collectors.toList())
Coletar em Map
collect
collect(Collectors.toMap(...))
Agrupamento
collect
collect(Collectors.groupingBy(...))
Concatenação de strings reduce / collect reduce("", String::concat) ou Collectors.joining()

4. Tarefas práticas

Tarefa 1: Encontrar a soma dos comprimentos de todas as strings na lista

List<String> words = List.of("gato", "cachorro", "elefante");

int totalLength = words.stream()
    .mapToInt(String::length)
    .sum(); // ou via reduce: .reduce(0, Integer::sum)

System.out.println(totalLength); // 20

Tarefa 2: Coletar elementos únicos em Set

List<String> fruits = List.of("maçã", "pera", "maçã", "laranja");

Set<String> uniqueFruits = fruits.stream()
    .collect(Collectors.toSet());

System.out.println(uniqueFruits); // [maçã, pera, laranja]

Tarefa 3: Construir um Map a partir de uma lista de objetos

List<Employee> employees = List.of(
    new Employee("Anya", "IT"),
    new Employee("Boris", "HR"),
    new Employee("Vasya", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // se os nomes coincidirem
    ));

System.out.println(nameToDept);

Tarefa 4: Juntar todos os nomes separados por vírgula

String allNames = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.joining(", "));

System.out.println(allNames); // Anya, Boris, Vasya

5. Particularidades de implementação e nuances

Optional e reduce

Se você usar reduce sem identity, o resultado é um Optional. Isso é seguro: se o stream estiver vazio, o resultado também estará vazio. Lembre-se de tratá-lo corretamente: ifPresent(...), orElse(...), orElseThrow(...).

Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);

Coletores próprios: se você quiser ir ao extremo

Você pode escrever seu próprio Collector caso os padrão não sejam suficientes. Mas para 99% dos casos, os prontos de Collectors são suficientes.

Coletores e streams paralelos

Os coletores prontos de Collectors são projetados para funcionar corretamente com parallelStream(). Não adicione elementos manualmente a uma coleção mutável compartilhada dentro de forEach em um stream paralelo — isso gera condições de corrida.

6. Erros típicos ao trabalhar com reduce e collect

Erro nº 1: Não verificar o Optional após reduce. Se o stream estiver vazio, reduce sem identity retorna um Optional vazio. Chamar get() causará NoSuchElementException. Use ifPresent, orElse ou orElseThrow.

Erro nº 2: Tentar coletar uma coleção via reduce. É possível, mas collect é mais adequado e rápido para isso:

// Ineficiente!
List<String> list = stream.reduce(
    new ArrayList<>(),
    (acc, elem) -> { acc.add(elem); return acc; },
    (acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// Melhor assim:
List<String> list2 = stream.collect(Collectors.toList());

Erro nº 3: Não tratar chaves duplicadas em toMap. Se as chaves coincidirem, haverá exceção. Adicione o terceiro argumento em toMap para resolver conflitos.

Erro nº 4: Usar coleções mutáveis em streams paralelos sem sincronização. Em collect, use os coletores padrão — eles funcionam corretamente em modo paralelo. Não faça list.add() dentro de forEach em parallelStream().

Erro nº 5: Confundir reduce e collect para tarefas complexas. reduce — para agregações simples (soma, máximo). collect — para coleta em coleções, agrupamentos, construção de Map e agregações complexas.

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