1. Método reduce: una reducción universal
En programación a menudo necesitamos «reducir» una colección a un único valor final: calcular una suma, hallar un producto, concatenar cadenas, computar una métrica agregada o recopilar elementos en una nueva estructura. Antes esto se hacía manualmente con bucles y una variable acumuladora. Hoy el API de Stream ofrece formas elegantes —los métodos universales reduce y collect— que permiten escribir código de forma compacta y declarativa.
- reduce reduce el stream a un único valor final (suma, producto, concatenación, etc.).
- collect transforma el stream en una colección, cadena, mapa o una estructura arbitraria.
Veámoslo paso a paso.
¿Para qué sirve reduce?
reduce es un método terminal que «reduce» los elementos del stream a un único valor utilizando una función acumuladora. Mentalmente equivale a recorrer la colección acumulando el resultado en cada paso.
Firmas del método reduce
En el API de Stream hay tres variantes principales 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: función que recibe el valor acumulado actual y el siguiente elemento, y devuelve el nuevo resultado.
- identity: valor inicial del acumulador (por ejemplo, 0 para suma, 1 para producto).
- combiner: se usa en streams paralelos para unir resultados intermedios.
Ejemplos de uso de reduce
Ejemplo 1: Suma de números
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// reduce sin identity — el resultado es Optional
Optional<Integer> sum1 = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println(sum1.orElse(0)); // 15
// reduce con identity — el resultado siempre existe
int sum2 = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum2); // 15
Ejemplo 2: Producto de todos los números
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println(product); // 120
Ejemplo 3: Concatenación de cadenas
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
Ejemplo 4: Búsqueda del elemento máximo
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(System.out::println); // 5
Ejemplo 5: Suma de las longitudes de todas las cadenas
List<String> texts = List.of("gato", "perro", "elefante");
int totalLength = texts.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(totalLength); // 17
Cómo funciona reduce
La lógica de reduce es equivalente al siguiente bucle:
T result = identity;
for (T element : collection) {
result = accumulator.apply(result, element);
}
return result;
Si no se indica identity, el valor inicial es el primer elemento del stream y el método devuelve un Optional (puede estar vacío si el stream está vacío).
2. Método collect: transformación universal
collect es un método terminal que convierte un stream en una colección, cadena, mapa o cualquier otra estructura. Para ello se usan «colectores» (Collector) que describen el proceso de recolección. Lo más habitual es usar los que ya vienen en la clase Collectors.
Colectores más populares
- Collectors.toList() — recopila los elementos en un List.
- Collectors.toSet() — recopila los elementos en un Set.
- Collectors.toMap() — recopila los elementos en un Map.
- Collectors.joining() — une cadenas en una sola.
- Collectors.groupingBy() — agrupa los elementos por un criterio.
- Collectors.counting() — cuenta la cantidad de elementos.
- Collectors.summarizingInt() — recopila estadísticas de números (suma, media, mín/máx).
Ejemplos de uso de collect
Ejemplo 1: Recolectar en una lista
List<String> names = List.of("Ana", "Boris", "Basil", "Ana");
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames); // [Ana, Boris, Basil]
Ejemplo 2: Recolectar en un conjunto (Set)
Set<String> nameSet = names.stream()
.collect(Collectors.toSet());
System.out.println(nameSet); // [Ana, Boris, Basil] (el orden no está garantizado)
Ejemplo 3: Recolectar en una cadena
String csv = names.stream()
.collect(Collectors.joining(", "));
System.out.println(csv); // Ana, Boris, Basil, Ana
Ejemplo 4: Recolectar en un Map
Supongamos que tenemos una clase:
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; }
}
Construyamos un mapa «nombre → departamento»:
List<Employee> employees = List.of(
new Employee("Ana", "IT"),
new Employee("Boris", "HR"),
new Employee("Basil", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // manejo de duplicados de nombres
));
System.out.println(nameToDept); // {Ana=IT, Boris=HR, Basil=IT}
Ejemplo 5: Recolectar elementos únicos en un Set
Set<String> unique = names.stream()
.collect(Collectors.toSet());
System.out.println(unique);
Ejemplo 6: Recopilar estadísticas sobre 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. Comparación: cuándo usar reduce y cuándo collect
reduce — cuando necesitas obtener un único valor final mediante una operación binaria: suma, producto, máximo, concatenación.
collect — cuando necesitas recopilar elementos en una colección/Mapa/cadena o realizar una agregación más compleja con un Collector. Para estas tareas, collect suele ser más potente y eficiente.
Tabla: reduce vs collect
| Tarea | Qué usar | Ejemplo |
|---|---|---|
| Suma de números | |
|
| Producto | |
|
| Recolección en List | |
|
| Recolección en Map | |
|
| Agrupación | |
|
| Concatenación de cadenas | reduce / collect | reduce("", String::concat) o Collectors.joining() |
4. Tareas prácticas
Tarea 1: Encontrar la suma de las longitudes de todas las cadenas de la lista
List<String> words = List.of("gato", "perro", "elefante");
int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // o mediante reduce: .reduce(0, Integer::sum)
System.out.println(totalLength); // 17
Tarea 2: Recopilar elementos únicos en un Set
List<String> fruits = List.of("manzana", "pera", "manzana", "naranja");
Set<String> uniqueFruits = fruits.stream()
.collect(Collectors.toSet());
System.out.println(uniqueFruits); // [manzana, pera, naranja]
Tarea 3: Construir un Map a partir de una lista de objetos
List<Employee> employees = List.of(
new Employee("Ana", "IT"),
new Employee("Boris", "HR"),
new Employee("Basil", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // si los nombres coinciden
));
System.out.println(nameToDept);
Tarea 4: Recolectar todos los nombres separados por comas
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println(allNames); // Ana, Boris, Basil
5. Particularidades de implementación y matices
Optional y reduce
Si se usa reduce sin identity, el resultado es un Optional. Esto es seguro: si el stream está vacío, el resultado también lo está. No olvides gestionarlo correctamente: ifPresent(...), orElse(...), orElseThrow(...).
Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);
Colectores propios: si necesitas algo extremo
Puedes escribir tu propio Collector si los estándar no son suficientes. Pero para el 99% de los casos bastan los que ofrece Collectors.
Colectores y streams paralelos
Los colectores de Collectors están diseñados para funcionar correctamente con parallelStream(). No añadas elementos manualmente a una colección mutables compartida dentro de forEach en un stream paralelo — te encontrarás con condiciones de carrera.
6. Errores típicos al trabajar con reduce y collect
Error n.º 1: No compruebas Optional tras reduce. Si el stream está vacío, reduce sin identity devuelve un Optional vacío. Llamar a get() provocará un NoSuchElementException. Usa ifPresent, orElse u orElseThrow.
Error n.º 2: Intentas recopilar una colección con reduce. Se puede hacer, pero collect está pensado para ello y suele ser mejor y más rápido:
// ¡Ineficiente!
List<String> list = stream.reduce(
new ArrayList<>(),
(acc, elem) -> { acc.add(elem); return acc; },
(acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// Mejor así:
List<String> list2 = stream.collect(Collectors.toList());
Error n.º 3: No manejas claves duplicadas en toMap. Si las claves coinciden, habrá una excepción. Añade el tercer argumento en toMap para resolver los conflictos.
Error n.º 4: Usas colecciones mutables en streams paralelos sin sincronización. En collect usa los colectores estándar: funcionan correctamente en modo paralelo. No hagas list.add() en forEach sobre un parallelStream().
Error n.º 5: Confundes reduce y collect para tareas complejas. reduce es para agregaciones simples (suma, máximo). collect es para recolectar en colecciones, agrupar, construir Map y realizar agregaciones complejas.
GO TO FULL VERSION