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ícate

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.

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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Java online

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

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

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

Accede GRATIS a Java y certifícate

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.