Java
Tutorial Java: Reducción y acumulación
Aprende a usar count(), min(), max() y reduce() en Java para operaciones de reducción y acumulación en streams de forma eficiente y práctica.
Aprende Java y certifícatecount()
El método count() es una operación terminal en los streams de Java que permite contabilizar el número de elementos que cumplen ciertos criterios. Esta operación forma parte del conjunto de operaciones de reducción que transforman un stream en un único valor.
La operación count() es especialmente útil cuando necesitamos conocer la cantidad de elementos en un stream, ya sea en su totalidad o después de aplicar algún filtro. A diferencia de otras operaciones de reducción como min() o max(), count() siempre devuelve un valor de tipo long
, independientemente del tipo de elementos en el stream.
La sintaxis básica de count() es muy sencilla:
long cantidad = stream.count();
Esta operación es determinista y no requiere ningún parámetro adicional, lo que la hace muy fácil de utilizar. Veamos algunos ejemplos prácticos:
Contando todos los elementos de un stream
El uso más básico de count() es contar todos los elementos presentes en un stream:
import java.util.List;
public class ContadorBasico {
public static void main(String[] args) {
List<String> nombres = List.of("Ana", "Juan", "María", "Carlos", "Elena");
long totalNombres = nombres.stream()
.count();
System.out.println("Total de nombres: " + totalNombres);
// Imprime: Total de nombres: 5
}
}
Contando elementos que cumplen una condición
Donde count() realmente muestra su utilidad es cuando se combina con otras operaciones intermedias como filter():
import java.util.List;
public class ContadorConFiltro {
public static void main(String[] args) {
List<String> nombres = List.of("Ana", "Juan", "María", "Carlos", "Elena");
// Contar nombres que empiezan con 'A'
long nombresConA = nombres.stream()
.filter(nombre -> nombre.startsWith("A"))
.count();
System.out.println("Nombres que empiezan con A: " + nombresConA);
// Imprime: Nombres que empiezan con A: 1
// Contar nombres con más de 4 letras
long nombresLargos = nombres.stream()
.filter(nombre -> nombre.length() > 4)
.count();
System.out.println("Nombres con más de 4 letras: " + nombresLargos);
// Imprime: Nombres con más de 4 letras: 2
}
}
Contando elementos en streams numéricos
Para streams de tipos primitivos, count() funciona de la misma manera:
import java.util.stream.IntStream;
public class ContadorNumerico {
public static void main(String[] args) {
// Contar números pares entre 1 y 100
long cantidadPares = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0)
.count();
System.out.println("Cantidad de números pares entre 1 y 100: " + cantidadPares);
// Imprime: Cantidad de números pares entre 1 y 100: 50
// Contar números primos entre 1 y 50
long cantidadPrimos = IntStream.rangeClosed(2, 50)
.filter(ContadorNumerico::esPrimo)
.count();
System.out.println("Cantidad de números primos entre 1 y 50: " + cantidadPrimos);
// Imprime: Cantidad de números primos entre 1 y 50: 15
}
// Método auxiliar para verificar si un número es primo
private static boolean esPrimo(int numero) {
return IntStream.rangeClosed(2, (int) Math.sqrt(numero))
.noneMatch(divisor -> numero % divisor == 0);
}
}
Contando elementos en streams de objetos complejos
El método count() también es útil cuando trabajamos con colecciones de objetos más complejos:
import java.util.List;
class Producto {
private String nombre;
private double precio;
private String categoria;
public Producto(String nombre, double precio, String categoria) {
this.nombre = nombre;
this.precio = precio;
this.categoria = categoria;
}
public String getNombre() { return nombre; }
public double getPrecio() { return precio; }
public String getCategoria() { return categoria; }
}
public class ContadorProductos {
public static void main(String[] args) {
List<Producto> productos = List.of(
new Producto("Laptop", 1200.0, "Electrónica"),
new Producto("Smartphone", 800.0, "Electrónica"),
new Producto("Libro", 25.0, "Libros"),
new Producto("Camiseta", 20.0, "Ropa"),
new Producto("Pantalón", 35.0, "Ropa")
);
// Contar productos de electrónica
long productosElectronica = productos.stream()
.filter(p -> p.getCategoria().equals("Electrónica"))
.count();
System.out.println("Productos de electrónica: " + productosElectronica);
// Imprime: Productos de electrónica: 2
// Contar productos con precio mayor a 50
long productosCaros = productos.stream()
.filter(p -> p.getPrecio() > 50)
.count();
System.out.println("Productos con precio mayor a 50: " + productosCaros);
// Imprime: Productos con precio mayor a 50: 2
}
}
Consideraciones de rendimiento
El método count() es una operación eficiente en términos de rendimiento, especialmente cuando se trabaja con streams paralelos:
import java.util.stream.IntStream;
import java.time.Duration;
import java.time.Instant;
public class RendimientoCount {
public static void main(String[] args) {
int limite = 100_000_000;
// Medición con stream secuencial
Instant inicio = Instant.now();
long conteoSecuencial = IntStream.range(0, limite)
.filter(n -> n % 2 == 0)
.count();
Instant fin = Instant.now();
System.out.println("Conteo secuencial: " + conteoSecuencial);
System.out.println("Tiempo secuencial: " + Duration.between(inicio, fin).toMillis() + " ms");
// Medición con stream paralelo
inicio = Instant.now();
long conteoParalelo = IntStream.range(0, limite)
.parallel()
.filter(n -> n % 2 == 0)
.count();
fin = Instant.now();
System.out.println("Conteo paralelo: " + conteoParalelo);
System.out.println("Tiempo paralelo: " + Duration.between(inicio, fin).toMillis() + " ms");
}
}
Casos de uso prácticos
El método count() resulta especialmente útil en escenarios como:
- Análisis de datos: Contar registros que cumplen ciertos criterios.
- Validación: Verificar si existen elementos que cumplan una condición.
- Estadísticas: Obtener métricas sobre conjuntos de datos.
Por ejemplo, podemos usar count() para implementar una función que verifique si una contraseña es segura:
public class ValidadorPassword {
public static boolean esPasswordSeguro(String password) {
// Una contraseña segura debe tener al menos:
// - 8 caracteres
// - 1 letra mayúscula
// - 1 letra minúscula
// - 1 número
if (password.length() < 8) {
return false;
}
long mayusculas = password.chars()
.filter(Character::isUpperCase)
.count();
long minusculas = password.chars()
.filter(Character::isLowerCase)
.count();
long numeros = password.chars()
.filter(Character::isDigit)
.count();
return mayusculas >= 1 && minusculas >= 1 && numeros >= 1;
}
public static void main(String[] args) {
String password1 = "abc123";
String password2 = "Abc12345";
System.out.println("Password1 es seguro: " + esPasswordSeguro(password1));
// Imprime: Password1 es seguro: false
System.out.println("Password2 es seguro: " + esPasswordSeguro(password2));
// Imprime: Password2 es seguro: true
}
}
En resumen, el método count() es una herramienta fundamental en la programación funcional con Java que nos permite obtener información cuantitativa sobre nuestros datos de manera concisa y eficiente. Su simplicidad de uso y su capacidad para combinarse con otras operaciones de stream lo convierten en un componente esencial para el procesamiento de datos.
min() y max()
Las operaciones min() y max() son métodos terminales en los streams de Java que permiten encontrar el valor mínimo y máximo respectivamente dentro de una secuencia de elementos. Estas operaciones forman parte del conjunto de operaciones de reducción que transforman un stream en un único valor.
A diferencia de otras operaciones terminales como count(), los métodos min() y max() requieren un Comparator que defina el criterio de ordenación para determinar qué elemento es menor o mayor. Ambos métodos devuelven un resultado encapsulado en un Optional, lo que permite manejar adecuadamente el caso en que el stream esté vacío.
La sintaxis básica de estos métodos es:
Optional<T> minimo = stream.min(comparator);
Optional<T> maximo = stream.max(comparator);
Uso básico con tipos primitivos
Para streams de tipos primitivos como IntStream, DoubleStream o LongStream, estos métodos ya tienen implementaciones que no requieren un comparador explícito:
import java.util.stream.IntStream;
public class MinMaxPrimitivos {
public static void main(String[] args) {
IntStream numeros = IntStream.of(5, 3, 9, 1, 7, 4, 6, 8, 2);
// Encontrar el valor mínimo
int minimo = numeros.min().orElse(0);
System.out.println("Valor mínimo: " + minimo);
// Imprime: Valor mínimo: 1
// Necesitamos crear un nuevo stream ya que el anterior se consumió
int maximo = IntStream.of(5, 3, 9, 1, 7, 4, 6, 8, 2)
.max()
.orElse(0);
System.out.println("Valor máximo: " + maximo);
// Imprime: Valor máximo: 9
}
}
Uso con objetos y comparadores
Para streams de objetos, necesitamos proporcionar un comparador que defina el criterio de ordenación:
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class MinMaxObjetos {
public static void main(String[] args) {
List<String> palabras = List.of("manzana", "pera", "uva", "sandía", "kiwi");
// Encontrar la palabra alfabéticamente primera
Optional<String> primera = palabras.stream()
.min(String::compareTo);
primera.ifPresent(p -> System.out.println("Primera palabra: " + p));
// Imprime: Primera palabra: kiwi
// Encontrar la palabra alfabéticamente última
Optional<String> ultima = palabras.stream()
.max(String::compareTo);
ultima.ifPresent(u -> System.out.println("Última palabra: " + u));
// Imprime: Última palabra: uva
// Encontrar la palabra más corta
Optional<String> masCorta = palabras.stream()
.min(Comparator.comparing(String::length));
masCorta.ifPresent(c -> System.out.println("Palabra más corta: " + c));
// Imprime: Palabra más corta: uva
// Encontrar la palabra más larga
Optional<String> masLarga = palabras.stream()
.max(Comparator.comparing(String::length));
masLarga.ifPresent(l -> System.out.println("Palabra más larga: " + l));
// Imprime: Palabra más larga: manzana
}
}
Trabajando con objetos complejos
Estos métodos son especialmente útiles cuando trabajamos con colecciones de objetos complejos:
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
class Producto {
private String nombre;
private double precio;
private int stock;
public Producto(String nombre, double precio, int stock) {
this.nombre = nombre;
this.precio = precio;
this.stock = stock;
}
public String getNombre() { return nombre; }
public double getPrecio() { return precio; }
public int getStock() { return stock; }
@Override
public String toString() {
return nombre + " ($" + precio + ")";
}
}
public class MinMaxProductos {
public static void main(String[] args) {
List<Producto> productos = List.of(
new Producto("Laptop", 1200.0, 5),
new Producto("Smartphone", 800.0, 10),
new Producto("Auriculares", 150.0, 20),
new Producto("Tablet", 350.0, 8),
new Producto("Monitor", 250.0, 15)
);
// Producto más caro
Optional<Producto> masCaro = productos.stream()
.max(Comparator.comparing(Producto::getPrecio));
masCaro.ifPresent(p -> System.out.println("Producto más caro: " + p));
// Imprime: Producto más caro: Laptop ($1200.0)
// Producto más barato
Optional<Producto> masBarato = productos.stream()
.min(Comparator.comparing(Producto::getPrecio));
masBarato.ifPresent(p -> System.out.println("Producto más barato: " + p));
// Imprime: Producto más barato: Auriculares ($150.0)
// Producto con mayor stock
Optional<Producto> mayorStock = productos.stream()
.max(Comparator.comparing(Producto::getStock));
mayorStock.ifPresent(p -> System.out.println("Producto con mayor stock: " + p));
// Imprime: Producto con mayor stock: Auriculares ($150.0)
}
}
Comparadores compuestos
Podemos crear comparadores más complejos combinando múltiples criterios:
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
class Empleado {
private String nombre;
private String departamento;
private double salario;
private int antiguedad;
public Empleado(String nombre, String departamento, double salario, int antiguedad) {
this.nombre = nombre;
this.departamento = departamento;
this.salario = salario;
this.antiguedad = antiguedad;
}
public String getNombre() { return nombre; }
public String getDepartamento() { return departamento; }
public double getSalario() { return salario; }
public int getAntiguedad() { return antiguedad; }
@Override
public String toString() {
return nombre + " (" + departamento + ", $" + salario + ", " + antiguedad + " años)";
}
}
public class MinMaxComparadorCompuesto {
public static void main(String[] args) {
List<Empleado> empleados = List.of(
new Empleado("Ana", "Ventas", 45000, 3),
new Empleado("Carlos", "IT", 55000, 5),
new Empleado("Elena", "IT", 60000, 2),
new Empleado("Juan", "Ventas", 48000, 7),
new Empleado("María", "RRHH", 50000, 4)
);
// Empleado mejor pagado por departamento
System.out.println("Empleado mejor pagado de IT:");
empleados.stream()
.filter(e -> e.getDepartamento().equals("IT"))
.max(Comparator.comparing(Empleado::getSalario))
.ifPresent(System.out::println);
// Imprime: Elena (IT, $60000.0, 2 años)
// Empleado con mayor antigüedad y mejor salario
Comparator<Empleado> porAntiguedadYSalario =
Comparator.comparing(Empleado::getAntiguedad)
.thenComparing(Empleado::getSalario, Comparator.reverseOrder());
System.out.println("Empleado con mayor antigüedad (y mejor salario en caso de empate):");
empleados.stream()
.max(porAntiguedadYSalario)
.ifPresent(System.out::println);
// Imprime: Juan (Ventas, $48000.0, 7 años)
}
}
Manejo de streams vacíos
Es importante manejar adecuadamente el caso en que un stream esté vacío:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
public class MinMaxStreamVacio {
public static void main(String[] args) {
List<Integer> listaVacia = new ArrayList<>();
// Intentar encontrar el mínimo en un stream vacío
Optional<Integer> minimo = listaVacia.stream()
.min(Integer::compare);
// Usando isPresent para verificar si hay un valor
if (minimo.isPresent()) {
System.out.println("Valor mínimo: " + minimo.get());
} else {
System.out.println("No hay valor mínimo (stream vacío)");
}
// Imprime: No hay valor mínimo (stream vacío)
// Usando orElse para proporcionar un valor predeterminado
int valorMaximo = listaVacia.stream()
.max(Integer::compare)
.orElse(-1);
System.out.println("Valor máximo (o -1 si no hay): " + valorMaximo);
// Imprime: Valor máximo (o -1 si no hay): -1
// Usando orElseThrow para lanzar una excepción
try {
int valor = listaVacia.stream()
.min(Integer::compare)
.orElseThrow(() -> new RuntimeException("Stream vacío"));
} catch (RuntimeException e) {
System.out.println("Excepción capturada: " + e.getMessage());
// Imprime: Excepción capturada: Stream vacío
}
}
}
Aplicaciones prácticas
Los métodos min() y max() son útiles en muchos escenarios del mundo real:
import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
class Transaccion {
private LocalDate fecha;
private String concepto;
private double importe;
public Transaccion(LocalDate fecha, String concepto, double importe) {
this.fecha = fecha;
this.concepto = concepto;
this.importe = importe;
}
public LocalDate getFecha() { return fecha; }
public String getConcepto() { return concepto; }
public double getImporte() { return importe; }
@Override
public String toString() {
return fecha + " - " + concepto + ": $" + importe;
}
}
public class AplicacionesMinMax {
public static void main(String[] args) {
List<Transaccion> transacciones = List.of(
new Transaccion(LocalDate.of(2023, 5, 15), "Compra supermercado", -120.50),
new Transaccion(LocalDate.of(2023, 5, 20), "Nómina", 1800.00),
new Transaccion(LocalDate.of(2023, 5, 22), "Factura luz", -85.30),
new Transaccion(LocalDate.of(2023, 5, 25), "Venta artículo", 250.00),
new Transaccion(LocalDate.of(2023, 5, 28), "Restaurante", -45.75)
);
// Encontrar la transacción con mayor importe (ingreso más alto)
Optional<Transaccion> mayorIngreso = transacciones.stream()
.max(Comparator.comparing(Transaccion::getImporte));
mayorIngreso.ifPresent(t -> System.out.println("Mayor ingreso: " + t));
// Imprime: Mayor ingreso: 2023-05-20 - Nómina: $1800.0
// Encontrar la transacción con menor importe (gasto más alto)
Optional<Transaccion> mayorGasto = transacciones.stream()
.min(Comparator.comparing(Transaccion::getImporte));
mayorGasto.ifPresent(t -> System.out.println("Mayor gasto: " + t));
// Imprime: Mayor gasto: 2023-05-15 - Compra supermercado: $-120.5
// Encontrar la transacción más reciente
Optional<Transaccion> masReciente = transacciones.stream()
.max(Comparator.comparing(Transaccion::getFecha));
masReciente.ifPresent(t -> System.out.println("Transacción más reciente: " + t));
// Imprime: Transacción más reciente: 2023-05-28 - Restaurante: $-45.75
}
}
Combinación con otras operaciones de stream
Los métodos min() y max() se pueden combinar con otras operaciones de stream para realizar análisis más complejos:
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
class Estudiante {
private String nombre;
private String curso;
private double calificacion;
public Estudiante(String nombre, String curso, double calificacion) {
this.nombre = nombre;
this.curso = curso;
this.calificacion = calificacion;
}
public String getNombre() { return nombre; }
public String getCurso() { return curso; }
public double getCalificacion() { return calificacion; }
@Override
public String toString() {
return nombre + " (" + curso + "): " + calificacion;
}
}
public class MinMaxAvanzado {
public static void main(String[] args) {
List<Estudiante> estudiantes = List.of(
new Estudiante("Ana", "Matemáticas", 8.5),
new Estudiante("Carlos", "Física", 7.2),
new Estudiante("Elena", "Matemáticas", 9.3),
new Estudiante("Juan", "Física", 8.7),
new Estudiante("María", "Química", 9.1),
new Estudiante("Pedro", "Química", 6.8)
);
// Encontrar el mejor estudiante de cada curso
Map<String, Optional<Estudiante>> mejoresPorCurso = estudiantes.stream()
.collect(Collectors.groupingBy(
Estudiante::getCurso,
Collectors.maxBy(Comparator.comparing(Estudiante::getCalificacion))
));
System.out.println("Mejores estudiantes por curso:");
mejoresPorCurso.forEach((curso, estudiante) ->
estudiante.ifPresent(e -> System.out.println(curso + ": " + e.getNombre() + " - " + e.getCalificacion()))
);
// Imprime:
// Matemáticas: Elena - 9.3
// Física: Juan - 8.7
// Química: María - 9.1
// Encontrar el estudiante con mejor calificación entre los que tienen más de 8
Optional<Estudiante> mejorSobresaliente = estudiantes.stream()
.filter(e -> e.getCalificacion() > 8.0)
.max(Comparator.comparing(Estudiante::getCalificacion));
mejorSobresaliente.ifPresent(e ->
System.out.println("Mejor estudiante sobresaliente: " + e)
);
// Imprime: Mejor estudiante sobresaliente: Elena (Matemáticas): 9.3
}
}
Los métodos min() y max() son herramientas fundamentales en la programación funcional con Java que nos permiten encontrar valores extremos en nuestras colecciones de datos. Su flexibilidad para trabajar con diferentes tipos de comparadores los hace extremadamente versátiles para resolver una amplia variedad de problemas de análisis de datos.
reduce()
La operación reduce() es una de las herramientas más potentes y flexibles para la programación funcional en Java. Esta operación terminal permite combinar todos los elementos de un stream en un único resultado mediante la aplicación repetida de una operación binaria.
A diferencia de operaciones como count(), min() o max(), que tienen propósitos específicos, reduce() es una operación de propósito general que nos permite implementar prácticamente cualquier tipo de acumulación o reducción personalizada.
En Java, existen tres variantes del método reduce():
// Variante 1: Con valor de identidad y operador binario
T resultado = stream.reduce(valorIdentidad, operadorBinario);
// Variante 2: Solo con operador binario (devuelve Optional)
Optional<T> resultado = stream.reduce(operadorBinario);
// Variante 3: Con identidad, acumulador y combinador (para streams paralelos)
U resultado = stream.reduce(valorIdentidad, acumulador, combinador);
Reducción con valor de identidad
La forma más común de usar reduce() incluye un valor de identidad y un operador binario:
import java.util.List;
public class ReduceBasico {
public static void main(String[] args) {
List<Integer> numeros = List.of(1, 2, 3, 4, 5);
// Sumar todos los números
int suma = numeros.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Suma: " + suma); // Imprime: Suma: 15
// Multiplicar todos los números
int producto = numeros.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("Producto: " + producto); // Imprime: Producto: 120
}
}
En este ejemplo:
- Para la suma, el valor de identidad es 0 (elemento neutro de la suma)
- Para la multiplicación, el valor de identidad es 1 (elemento neutro de la multiplicación)
- El operador binario es una expresión lambda que define cómo combinar dos elementos
Reducción sin valor de identidad
Cuando no proporcionamos un valor de identidad, reduce() devuelve un Optional que puede estar vacío si el stream no contiene elementos:
import java.util.List;
import java.util.Optional;
public class ReduceSinIdentidad {
public static void main(String[] args) {
List<Integer> numeros = List.of(1, 2, 3, 4, 5);
// Encontrar el máximo usando reduce
Optional<Integer> maximo = numeros.stream()
.reduce((a, b) -> a > b ? a : b);
maximo.ifPresent(max -> System.out.println("Máximo: " + max));
// Imprime: Máximo: 5
// Concatenar strings
List<String> palabras = List.of("Java", "es", "funcional");
Optional<String> frase = palabras.stream()
.reduce((a, b) -> a + " " + b);
frase.ifPresent(System.out::println);
// Imprime: Java es funcional
// Manejo de stream vacío
List<String> listaVacia = List.of();
Optional<String> resultado = listaVacia.stream()
.reduce((a, b) -> a + b);
System.out.println("¿Tiene resultado? " + resultado.isPresent());
// Imprime: ¿Tiene resultado? false
}
}
Esta variante es útil cuando no existe un valor de identidad natural o cuando queremos manejar explícitamente el caso de un stream vacío.
Casos de uso prácticos
El método reduce() es extremadamente versátil y puede aplicarse a una amplia variedad de problemas:
Encontrar el elemento más largo en una colección
import java.util.List;
import java.util.Optional;
public class ReduceElementoMasLargo {
public static void main(String[] args) {
List<String> palabras = List.of("manzana", "pera", "kiwi", "melocotón", "uva");
Optional<String> palabraMasLarga = palabras.stream()
.reduce((palabra1, palabra2) ->
palabra1.length() > palabra2.length() ? palabra1 : palabra2);
palabraMasLarga.ifPresent(p ->
System.out.println("Palabra más larga: " + p));
// Imprime: Palabra más larga: melocotón
}
}
Calcular estadísticas personalizadas
import java.util.List;
class Estadisticas {
private int contador;
private double suma;
public Estadisticas(int contador, double suma) {
this.contador = contador;
this.suma = suma;
}
public Estadisticas combinar(Estadisticas otras) {
return new Estadisticas(
this.contador + otras.contador,
this.suma + otras.suma
);
}
public Estadisticas añadirValor(double valor) {
return new Estadisticas(this.contador + 1, this.suma + valor);
}
public double getPromedio() {
return contador > 0 ? suma / contador : 0;
}
public int getContador() {
return contador;
}
public double getSuma() {
return suma;
}
}
public class ReduceEstadisticas {
public static void main(String[] args) {
List<Double> valores = List.of(4.5, 3.2, 5.0, 2.7, 4.8);
Estadisticas stats = valores.stream()
.reduce(
new Estadisticas(0, 0.0),
(acumulador, valor) -> acumulador.añadirValor(valor),
(est1, est2) -> est1.combinar(est2)
);
System.out.println("Cantidad: " + stats.getContador());
System.out.println("Suma: " + stats.getSuma());
System.out.println("Promedio: " + stats.getPromedio());
// Imprime:
// Cantidad: 5
// Suma: 20.2
// Promedio: 4.04
}
}
Unir elementos con un separador
import java.util.List;
import java.util.StringJoiner;
public class ReduceUnirElementos {
public static void main(String[] args) {
List<String> lenguajes = List.of("Java", "Python", "JavaScript", "C#", "Kotlin");
// Usando reduce para implementar un join con comas
String resultado = lenguajes.stream()
.reduce(
new StringJoiner(", "),
(joiner, lenguaje) -> joiner.add(lenguaje),
(joiner1, joiner2) -> joiner1.merge(joiner2)
).toString();
System.out.println("Lenguajes: " + resultado);
// Imprime: Lenguajes: Java, Python, JavaScript, C#, Kotlin
}
}
Reducción con tipos diferentes
La tercera variante de reduce() permite que el tipo del resultado sea diferente al tipo de los elementos del stream. Esto es útil para acumulaciones más complejas:
import java.util.List;
import java.util.HashMap;
import java.util.Map;
public class ReduceTiposDiferentes {
public static void main(String[] args) {
List<String> palabras = List.of("hola", "mundo", "java", "programación",
"funcional", "hola", "java");
// Contar frecuencia de palabras
Map<String, Integer> frecuencias = palabras.stream()
.reduce(
new HashMap<String, Integer>(),
(map, palabra) -> {
map.put(palabra, map.getOrDefault(palabra, 0) + 1);
return map;
},
(map1, map2) -> {
Map<String, Integer> resultado = new HashMap<>(map1);
map2.forEach((k, v) -> resultado.merge(k, v, Integer::sum));
return resultado;
}
);
System.out.println("Frecuencia de palabras:");
frecuencias.forEach((palabra, frecuencia) ->
System.out.println(palabra + ": " + frecuencia));
// Imprime:
// mundo: 1
// programación: 1
// funcional: 1
// hola: 2
// java: 2
}
}
Reducción con métodos referenciados
Podemos hacer nuestro código más conciso utilizando referencias a métodos:
import java.util.List;
public class ReduceMetodosReferenciados {
public static void main(String[] args) {
List<Integer> numeros = List.of(1, 2, 3, 4, 5);
// Suma usando referencia a método
int suma = numeros.stream()
.reduce(0, Integer::sum);
System.out.println("Suma: " + suma); // Imprime: Suma: 15
// Máximo usando referencia a método
int maximo = numeros.stream()
.reduce(Integer.MIN_VALUE, Integer::max);
System.out.println("Máximo: " + maximo); // Imprime: Máximo: 5
}
}
Reducción con streams paralelos
Una de las ventajas de reduce() es que funciona bien con streams paralelos, especialmente cuando usamos la tercera variante con un combinador:
import java.util.stream.LongStream;
import java.time.Duration;
import java.time.Instant;
public class ReduceParalelo {
public static void main(String[] args) {
long n = 10_000_000;
// Suma secuencial
Instant inicio = Instant.now();
long sumaSecuencial = LongStream.rangeClosed(1, n)
.reduce(0L, Long::sum);
Instant fin = Instant.now();
System.out.println("Suma secuencial: " + sumaSecuencial);
System.out.println("Tiempo: " + Duration.between(inicio, fin).toMillis() + " ms");
// Suma paralela
inicio = Instant.now();
long sumaParalela = LongStream.rangeClosed(1, n)
.parallel()
.reduce(0L, Long::sum);
fin = Instant.now();
System.out.println("Suma paralela: " + sumaParalela);
System.out.println("Tiempo: " + Duration.between(inicio, fin).toMillis() + " ms");
}
}
Ejemplo avanzado: Análisis de texto
Veamos un ejemplo más complejo que utiliza reduce() para analizar un texto:
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
class EstadisticasTexto {
private int palabras;
private int caracteres;
private Map<String, Long> frecuenciaPalabras;
private String palabraMasLarga;
public EstadisticasTexto() {
this.palabras = 0;
this.caracteres = 0;
this.frecuenciaPalabras = Map.of();
this.palabraMasLarga = "";
}
public EstadisticasTexto(int palabras, int caracteres,
Map<String, Long> frecuenciaPalabras,
String palabraMasLarga) {
this.palabras = palabras;
this.caracteres = caracteres;
this.frecuenciaPalabras = frecuenciaPalabras;
this.palabraMasLarga = palabraMasLarga;
}
public static EstadisticasTexto analizar(String texto) {
if (texto == null || texto.isEmpty()) {
return new EstadisticasTexto();
}
String[] palabras = texto.toLowerCase().split("\\W+");
Map<String, Long> frecuencia = Arrays.stream(palabras)
.filter(p -> !p.isEmpty())
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
String palabraMasLarga = Arrays.stream(palabras)
.filter(p -> !p.isEmpty())
.reduce("", (p1, p2) -> p1.length() >= p2.length() ? p1 : p2);
return new EstadisticasTexto(
(int) Arrays.stream(palabras).filter(p -> !p.isEmpty()).count(),
texto.replaceAll("\\s", "").length(),
frecuencia,
palabraMasLarga
);
}
@Override
public String toString() {
return "Estadísticas del texto:\n" +
"- Palabras: " + palabras + "\n" +
"- Caracteres: " + caracteres + "\n" +
"- Palabra más larga: " + palabraMasLarga + "\n" +
"- Top 3 palabras más frecuentes: " +
frecuenciaPalabras.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(3)
.map(e -> e.getKey() + " (" + e.getValue() + ")")
.collect(Collectors.joining(", "));
}
}
public class ReduceAnalisisTexto {
public static void main(String[] args) {
String texto = "Java es un lenguaje de programación orientado a objetos. " +
"Java fue desarrollado por Sun Microsystems. " +
"Java es ahora propiedad de Oracle. " +
"La programación funcional en Java se introdujo en Java 8.";
EstadisticasTexto estadisticas = EstadisticasTexto.analizar(texto);
System.out.println(estadisticas);
}
}
Consideraciones importantes
Al utilizar reduce(), es importante tener en cuenta algunas consideraciones:
- Asociatividad: El operador binario debe ser asociativo para que funcione correctamente con streams paralelos.
- Valor de identidad: Debe ser un elemento neutro para la operación (no afecta al resultado cuando se combina con otro elemento).
- Efectos secundarios: Evita efectos secundarios en las funciones lambda utilizadas en reduce().
- Mutabilidad: Ten cuidado al acumular en objetos mutables, especialmente con streams paralelos.
La operación reduce() es una herramienta fundamental en la programación funcional con Java que nos permite implementar prácticamente cualquier tipo de acumulación o reducción de datos. Su flexibilidad y potencia la convierten en una de las operaciones más versátiles del API Stream de Java.
Identidad y acumuladores
En el contexto de la programación funcional en Java, los conceptos de identidad y acumuladores son fundamentales para entender cómo funcionan las operaciones de reducción. Estos elementos son la base para transformar colecciones de datos en valores únicos de forma eficiente y elegante.
Valor de identidad
El valor de identidad es un elemento especial que, cuando se combina con cualquier otro elemento mediante una operación específica, devuelve ese otro elemento sin modificarlo. En términos matemáticos, es el elemento neutro de una operación.
Algunos ejemplos clásicos de valores de identidad son:
- 0 para la suma:
a + 0 = a
- 1 para la multiplicación:
a * 1 = a
- Cadena vacía para la concatenación:
a + "" = a
- Lista vacía para la unión de listas:
a ∪ [] = a
En Java, estos valores de identidad se utilizan principalmente en operaciones de reducción para proporcionar un valor inicial que no afecte al resultado final:
import java.util.List;
public class ValoresIdentidad {
public static void main(String[] args) {
List<Integer> numeros = List.of(5, 3, 8, 2, 7);
// Usando 0 como identidad para la suma
int suma = numeros.stream()
.reduce(0, Integer::sum);
System.out.println("Suma: " + suma); // Imprime: 25
// Usando 1 como identidad para la multiplicación
int producto = numeros.stream()
.reduce(1, (a, b) -> a * b);
System.out.println("Producto: " + producto); // Imprime: 1680
// Usando cadena vacía como identidad para concatenación
String concatenacion = numeros.stream()
.map(String::valueOf)
.reduce("", (a, b) -> a + b);
System.out.println("Concatenación: " + concatenacion); // Imprime: 53827
}
}
Propiedades de un buen valor de identidad
Para que un valor funcione correctamente como identidad en operaciones de reducción, debe cumplir la siguiente propiedad:
combinar(identidad, x) = x
Donde combinar
es la operación binaria utilizada en la reducción y x
es cualquier elemento del tipo apropiado.
Esta propiedad garantiza que el valor de identidad no distorsione el resultado cuando se utiliza como valor inicial en una reducción.
Acumuladores
Un acumulador es una función que combina un resultado parcial con el siguiente elemento de un stream para producir un nuevo resultado parcial. En Java, los acumuladores suelen implementarse como funciones binarias (que toman dos argumentos y devuelven un resultado).
Los acumuladores son el corazón de las operaciones de reducción, ya que definen cómo se van combinando los elementos para llegar al resultado final:
import java.util.List;
import java.util.function.BinaryOperator;
public class Acumuladores {
public static void main(String[] args) {
List<Integer> numeros = List.of(5, 3, 8, 2, 7);
// Acumulador para suma
BinaryOperator<Integer> acumuladorSuma = (acumulado, elemento) -> acumulado + elemento;
int suma = numeros.stream().reduce(0, acumuladorSuma);
System.out.println("Suma: " + suma); // Imprime: 25
// Acumulador para encontrar el máximo
BinaryOperator<Integer> acumuladorMax = (max, elemento) -> Math.max(max, elemento);
int maximo = numeros.stream().reduce(Integer.MIN_VALUE, acumuladorMax);
System.out.println("Máximo: " + maximo); // Imprime: 8
// Acumulador para contar elementos que cumplen una condición
BinaryOperator<Integer> acumuladorContador = (contador, elemento) ->
elemento % 2 == 0 ? contador + 1 : contador;
int pares = numeros.stream().reduce(0, acumuladorContador);
System.out.println("Cantidad de números pares: " + pares); // Imprime: 2
}
}
Acumuladores inmutables vs mutables
Los acumuladores pueden trabajar con valores inmutables (como en los ejemplos anteriores) o con objetos mutables. Cuando se trabaja con objetos mutables, es importante tener cuidado, especialmente en streams paralelos:
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
public class AcumuladoresMutables {
public static void main(String[] args) {
List<String> palabras = List.of("Java", "es", "un", "lenguaje", "funcional");
// Acumulador mutable (NO recomendado para streams paralelos)
BiFunction<List<String>, String, List<String>> acumuladorMutable =
(lista, palabra) -> {
if (palabra.length() > 2) {
lista.add(palabra);
}
return lista;
};
List<String> palabrasLargas = palabras.stream()
.reduce(
new ArrayList<>(), // Contenedor mutable
acumuladorMutable,
(lista1, lista2) -> {
lista1.addAll(lista2);
return lista1;
}
);
System.out.println("Palabras con más de 2 letras: " + palabrasLargas);
// Imprime: [Java, lenguaje, funcional]
// Versión inmutable (más segura para streams paralelos)
BiFunction<List<String>, String, List<String>> acumuladorInmutable =
(lista, palabra) -> {
List<String> nuevaLista = new ArrayList<>(lista);
if (palabra.length() > 2) {
nuevaLista.add(palabra);
}
return nuevaLista;
};
BinaryOperator<List<String>> combinador =
(lista1, lista2) -> {
List<String> resultado = new ArrayList<>(lista1);
resultado.addAll(lista2);
return resultado;
};
List<String> palabrasLargasInmutable = palabras.stream()
.reduce(
new ArrayList<>(),
acumuladorInmutable,
combinador
);
System.out.println("Palabras con más de 2 letras (inmutable): " + palabrasLargasInmutable);
// Imprime: [Java, lenguaje, funcional]
}
}
Combinadores para streams paralelos
Cuando trabajamos con streams paralelos, necesitamos un tercer componente además del valor de identidad y el acumulador: el combinador. Este es una función binaria que combina dos resultados parciales en uno solo.
El combinador es esencial para el procesamiento paralelo, ya que permite unir los resultados de diferentes subprocesos:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
public class Combinadores {
public static void main(String[] args) {
List<String> palabras = List.of(
"Java", "programación", "funcional", "stream", "reduce",
"identidad", "acumulador", "combinador", "paralelo"
);
// Acumulador: cuenta la frecuencia de la primera letra de cada palabra
BiFunction<Map<Character, Integer>, String, Map<Character, Integer>> acumulador =
(mapa, palabra) -> {
char primeraLetra = palabra.charAt(0);
Map<Character, Integer> nuevoMapa = new HashMap<>(mapa);
nuevoMapa.put(primeraLetra, nuevoMapa.getOrDefault(primeraLetra, 0) + 1);
return nuevoMapa;
};
// Combinador: une dos mapas de frecuencias
BinaryOperator<Map<Character, Integer>> combinador =
(mapa1, mapa2) -> {
Map<Character, Integer> resultado = new HashMap<>(mapa1);
mapa2.forEach((letra, frecuencia) ->
resultado.merge(letra, frecuencia, Integer::sum));
return resultado;
};
// Procesamiento secuencial
Map<Character, Integer> frecuenciaSecuencial = palabras.stream()
.reduce(
new HashMap<>(), // Identidad: mapa vacío
acumulador,
combinador
);
System.out.println("Frecuencia de primeras letras (secuencial): " + frecuenciaSecuencial);
// Procesamiento paralelo (mismo resultado, potencialmente más rápido)
Map<Character, Integer> frecuenciaParalela = palabras.parallelStream()
.reduce(
new HashMap<>(), // Identidad: mapa vacío
acumulador,
combinador
);
System.out.println("Frecuencia de primeras letras (paralelo): " + frecuenciaParalela);
}
}
Propiedades matemáticas importantes
Para que las operaciones de reducción funcionen correctamente, especialmente en streams paralelos, los acumuladores y combinadores deben cumplir ciertas propiedades matemáticas:
- Asociatividad:
(a op b) op c = a op (b op c)
- Identidad:
a op identidad = identidad op a = a
- No interferencia: Las funciones no deben modificar la fuente de datos
- Estaticidad: Las funciones no deben depender de estado externo que pueda cambiar
import java.util.List;
public class PropiedadesMatematicas {
public static void main(String[] args) {
List<Integer> numeros = List.of(1, 2, 3, 4, 5);
// Operación asociativa: suma
int sumaSecuencial = numeros.stream()
.reduce(0, Integer::sum);
int sumaParalela = numeros.parallelStream()
.reduce(0, Integer::sum);
System.out.println("Suma secuencial: " + sumaSecuencial);
System.out.println("Suma paralela: " + sumaParalela);
System.out.println("¿Son iguales? " + (sumaSecuencial == sumaParalela));
// Imprime: ¿Son iguales? true
// Operación NO asociativa: resta
int restaSecuencial = numeros.stream()
.reduce(0, (a, b) -> a - b);
int restaParalela = numeros.parallelStream()
.reduce(0, (a, b) -> a - b);
System.out.println("Resta secuencial: " + restaSecuencial);
System.out.println("Resta paralela: " + restaParalela);
System.out.println("¿Son iguales? " + (restaSecuencial == restaParalela));
// Imprime: ¿Son iguales? false (la resta no es asociativa)
}
}
Patrones comunes con identidad y acumuladores
Existen varios patrones comunes que utilizan identidad y acumuladores para resolver problemas específicos:
Patrón de recolección
Recolectar elementos que cumplen una condición en una estructura de datos:
import java.util.ArrayList;
import java.util.List;
public class PatronRecoleccion {
public static void main(String[] args) {
List<Integer> numeros = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Recolectar números pares en una lista
List<Integer> pares = numeros.stream()
.reduce(
new ArrayList<>(), // Identidad: lista vacía
(lista, numero) -> {
if (numero % 2 == 0) {
List<Integer> nuevaLista = new ArrayList<>(lista);
nuevaLista.add(numero);
return nuevaLista;
}
return lista;
},
(lista1, lista2) -> {
List<Integer> resultado = new ArrayList<>(lista1);
resultado.addAll(lista2);
return resultado;
}
);
System.out.println("Números pares: " + pares);
// Imprime: Números pares: [2, 4, 6, 8, 10]
}
}
Patrón de estadísticas
Calcular múltiples estadísticas en una sola pasada:
import java.util.List;
class ResumenEstadistico {
private final int contador;
private final double suma;
private final double minimo;
private final double maximo;
public ResumenEstadistico(int contador, double suma, double minimo, double maximo) {
this.contador = contador;
this.suma = suma;
this.minimo = minimo;
this.maximo = maximo;
}
public static ResumenEstadistico identidad() {
return new ResumenEstadistico(0, 0, Double.MAX_VALUE, Double.MIN_VALUE);
}
public ResumenEstadistico acumular(double valor) {
return new ResumenEstadistico(
contador + 1,
suma + valor,
Math.min(minimo, valor),
Math.max(maximo, valor)
);
}
public ResumenEstadistico combinar(ResumenEstadistico otro) {
return new ResumenEstadistico(
this.contador + otro.contador,
this.suma + otro.suma,
Math.min(this.minimo, otro.minimo),
Math.max(this.maximo, otro.maximo)
);
}
public double getPromedio() {
return contador > 0 ? suma / contador : 0;
}
@Override
public String toString() {
return String.format(
"Contador: %d, Suma: %.2f, Mínimo: %.2f, Máximo: %.2f, Promedio: %.2f",
contador, suma,
minimo != Double.MAX_VALUE ? minimo : 0,
maximo != Double.MIN_VALUE ? maximo : 0,
getPromedio()
);
}
}
public class PatronEstadisticas {
public static void main(String[] args) {
List<Double> valores = List.of(4.5, 3.2, 5.0, 2.7, 4.8);
ResumenEstadistico resumen = valores.stream()
.reduce(
ResumenEstadistico.identidad(),
(acumulado, valor) -> acumulado.acumular(valor),
(r1, r2) -> r1.combinar(r2)
);
System.out.println("Estadísticas: " + resumen);
// Imprime: Estadísticas: Contador: 5, Suma: 20.20, Mínimo: 2.70, Máximo: 5.00, Promedio: 4.04
}
}
Patrón de agrupación
Agrupar elementos por una clave y realizar operaciones en cada grupo:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Producto {
private final String categoria;
private final double precio;
public Producto(String categoria, double precio) {
this.categoria = categoria;
this.precio = precio;
}
public String getCategoria() { return categoria; }
public double getPrecio() { return precio; }
}
public class PatronAgrupacion {
public static void main(String[] args) {
List<Producto> productos = List.of(
new Producto("Electrónica", 850.0),
new Producto("Ropa", 45.0),
new Producto("Electrónica", 1200.0),
new Producto("Hogar", 120.0),
new Producto("Ropa", 35.0),
new Producto("Hogar", 75.0)
);
// Agrupar productos por categoría y sumar precios
Map<String, Double> ventasPorCategoria = productos.stream()
.reduce(
new HashMap<>(), // Identidad: mapa vacío
(mapa, producto) -> {
Map<String, Double> nuevoMapa = new HashMap<>(mapa);
String categoria = producto.getCategoria();
double precioActual = nuevoMapa.getOrDefault(categoria, 0.0);
nuevoMapa.put(categoria, precioActual + producto.getPrecio());
return nuevoMapa;
},
(mapa1, mapa2) -> {
Map<String, Double> resultado = new HashMap<>(mapa1);
mapa2.forEach((categoria, suma) ->
resultado.merge(categoria, suma, Double::sum));
return resultado;
}
);
System.out.println("Ventas por categoría:");
ventasPorCategoria.forEach((categoria, suma) ->
System.out.printf("%s: $%.2f%n", categoria, suma));
// Imprime:
// Electrónica: $2050.00
// Ropa: $80.00
// Hogar: $195.00
}
}
Aplicación práctica: Análisis de texto
Un ejemplo completo que utiliza identidad y acumuladores para analizar un texto:
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
class AnalisisTexto {
private final int totalPalabras;
private final int totalCaracteres;
private final Map<Integer, Integer> distribucionLongitud;
private final String palabraMasLarga;
public AnalisisTexto(int totalPalabras, int totalCaracteres,
Map<Integer, Integer> distribucionLongitud,
String palabraMasLarga) {
this.totalPalabras = totalPalabras;
this.totalCaracteres = totalCaracteres;
this.distribucionLongitud = distribucionLongitud;
this.palabraMasLarga = palabraMasLarga;
}
public static AnalisisTexto identidad() {
return new AnalisisTexto(0, 0, new HashMap<>(), "");
}
public AnalisisTexto acumular(String palabra) {
if (palabra == null || palabra.isEmpty()) {
return this;
}
int longitud = palabra.length();
Map<Integer, Integer> nuevaDistribucion = new HashMap<>(this.distribucionLongitud);
nuevaDistribucion.put(longitud, nuevaDistribucion.getOrDefault(longitud, 0) + 1);
return new AnalisisTexto(
this.totalPalabras + 1,
this.totalCaracteres + longitud,
nuevaDistribucion,
palabra.length() > this.palabraMasLarga.length() ? palabra : this.palabraMasLarga
);
}
public AnalisisTexto combinar(AnalisisTexto otro) {
Map<Integer, Integer> distribucionCombinada = new HashMap<>(this.distribucionLongitud);
otro.distribucionLongitud.forEach((longitud, cantidad) ->
distribucionCombinada.merge(longitud, cantidad, Integer::sum));
return new AnalisisTexto(
this.totalPalabras + otro.totalPalabras,
this.totalCaracteres + otro.totalCaracteres,
distribucionCombinada,
this.palabraMasLarga.length() >= otro.palabraMasLarga.length()
? this.palabraMasLarga : otro.palabraMasLarga
);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Análisis de texto:\n");
sb.append("- Total palabras: ").append(totalPalabras).append("\n");
sb.append("- Total caracteres: ").append(totalCaracteres).append("\n");
sb.append("- Longitud media: ").append(
totalPalabras > 0 ? String.format("%.2f", (double) totalCaracteres / totalPalabras) : "0"
).append("\n");
sb.append("- Palabra más larga: ").append(palabraMasLarga).append("\n");
sb.append("- Distribución por longitud:\n");
distribucionLongitud.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> sb.append(" ").append(e.getKey())
.append(" letras: ").append(e.getValue())
.append(" palabras\n"));
return sb.toString();
}
}
public class AplicacionAnalisisTexto {
public static void main(String[] args) {
String texto = "La programación funcional en Java permite escribir código más conciso, " +
"expresivo y menos propenso a errores. Las operaciones de reducción como " +
"reduce son fundamentales para transformar colecciones en valores únicos.";
String[] palabras = texto.split("\\W+");
AnalisisTexto analisis = Arrays.stream(palabras)
.filter(p -> !p.isEmpty())
.reduce(
AnalisisTexto.identidad(),
(a, palabra) -> a.acumular(palabra),
(a1, a2) -> a1.combinar(a2)
);
System.out.println(analisis);
}
}
Consideraciones de rendimiento
Al trabajar con identidad y acumuladores, es importante considerar el rendimiento:
- Los acumuladores inmutables son más seguros pero pueden generar más objetos temporales
- Los acumuladores mutables son más eficientes pero requieren cuidado en streams paralelos
- La asociatividad es crucial para obtener resultados correctos en procesamiento paralelo
- El tamaño de los datos determina si vale la pena usar streams paralelos
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class RendimientoAcumuladores {
public static void main(String[] args) {
// Generar una lista grande de números aleatorios
List<Integer> numeros = IntStream.range(0, 10_000_000)
.map(i -> ThreadLocalRandom.current().nextInt(100))
.boxed()
.collect(Collectors.toList());
// Usando reduce con acumulador inmutable
Instant inicio = Instant.now();
List<Integer> paresInmutable = numeros.parallelStream()
.reduce(
new ArrayList<>(),
(lista, numero) -> {
if (numero % 2 == 0) {
List<Integer> nuevaLista = new ArrayList<>(lista);
nuevaLista.add(numero);
return nuevaLista;
}
return lista;
},
(lista1, lista2) -> {
List<Integer> resultado = new ArrayList<>(lista1);
resultado.addAll(lista2);
return resultado;
}
);
Instant fin = Instant.now();
System.out.println("Tiempo con acumulador inmutable: " +
Duration.between(inicio, fin).toMillis() + " ms");
System.out.println("Cantidad de números pares encontrados: " + paresInmutable.size());
// Usando collect (más eficiente para este caso)
inicio = Instant.now();
List<Integer> paresCollect = numeros.parallelStream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
fin = Instant.now();
System.out.println("Tiempo con collect: " +
Duration.between(inicio, fin).toMillis() + " ms");
System.out.println("Cantidad de números pares encontrados: " + paresCollect.size());
}
}
Los conceptos de identidad y acumuladores son pilares fundamentales de la programación funcional en Java. Comprender cómo funcionan y cómo aplicarlos correctamente nos permite escribir código más expresivo, mantenible y eficiente para transformar colecciones de datos en resultados significativos.
Ejercicios de esta lección Reducción y acumulación
Evalúa tus conocimientos de esta lección Reducción y acumulación con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el uso y aplicación del método count() para contabilizar elementos en streams.
- Aprender a utilizar min() y max() para obtener valores extremos en colecciones, incluyendo el manejo de comparadores y Optional.
- Entender la operación reduce() para realizar acumulaciones personalizadas y su flexibilidad en programación funcional.
- Conocer los conceptos fundamentales de identidad y acumuladores, y su importancia en operaciones de reducción y procesamiento paralelo.
- Aplicar patrones comunes y buenas prácticas para optimizar el uso de operaciones de reducción en Java Streams.