count()
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.
¿Te está gustando esta lección?
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
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.
Aprendizajes 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.
Completa Java y certifícate
Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs