Java

Tutorial Java: Transformación

Aprende a usar las operaciones distinct, sorted, limit, skip y peek en Streams de Java para procesar datos de forma eficiente y funcional.

Aprende Java y certifícate

distinct()

La operación distinct() es una operación intermedia en los Streams de Java que permite eliminar elementos duplicados de una secuencia. Esta operación es especialmente útil cuando trabajamos con colecciones que pueden contener valores repetidos y necesitamos obtener solo valores únicos.

Cuando aplicamos distinct() a un Stream, este método crea un nuevo Stream que contiene los elementos del Stream original pero sin duplicados. Para determinar si dos elementos son iguales, distinct() utiliza el método equals() de los objetos, por lo que es importante que este método esté correctamente implementado en las clases de los objetos que procesamos.

Funcionamiento básico

La sintaxis para usar distinct() es muy sencilla:

Stream<T> distinct()

Veamos un ejemplo simple con números enteros:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class DistinctExample {
    public static void main(String[] args) {
        List<Integer> numeros = Arrays.asList(1, 2, 3, 2, 1, 4, 5, 4, 6);
        
        List<Integer> numerosUnicos = numeros.stream()
                                            .distinct()
                                            .collect(Collectors.toList());
        
        System.out.println("Lista original: " + numeros);
        System.out.println("Lista sin duplicados: " + numerosUnicos);
    }
}

La salida de este programa sería:

Lista original: [1, 2, 3, 2, 1, 4, 5, 4, 6]
Lista sin duplicados: [1, 2, 3, 4, 5, 6]

Como podemos observar, distinct() ha eliminado los números duplicados, dejando solo una ocurrencia de cada valor.

Uso con objetos personalizados

Cuando trabajamos con objetos personalizados, es fundamental asegurarnos de que los métodos equals() y hashCode() estén correctamente implementados para que distinct() funcione adecuadamente:

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

class Persona {
    private String nombre;
    private int edad;
    
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Persona persona = (Persona) o;
        return edad == persona.edad && Objects.equals(nombre, persona.nombre);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(nombre, edad);
    }
    
    @Override
    public String toString() {
        return "Persona{" + "nombre='" + nombre + "', edad=" + edad + '}';
    }
}

public class DistinctObjectExample {
    public static void main(String[] args) {
        List<Persona> personas = Arrays.asList(
            new Persona("Ana", 25),
            new Persona("Carlos", 30),
            new Persona("Ana", 25),  // Duplicado
            new Persona("Luis", 28),
            new Persona("Carlos", 30)  // Duplicado
        );
        
        List<Persona> personasUnicas = personas.stream()
                                             .distinct()
                                             .collect(Collectors.toList());
        
        System.out.println("Lista original: " + personas.size() + " personas");
        personas.forEach(System.out::println);
        
        System.out.println("\nLista sin duplicados: " + personasUnicas.size() + " personas");
        personasUnicas.forEach(System.out::println);
    }
}

En este ejemplo, dos objetos Persona se consideran iguales si tienen el mismo nombre y edad. La salida mostraría:

Lista original: 5 personas
Persona{nombre='Ana', edad=25}
Persona{nombre='Carlos', edad=30}
Persona{nombre='Ana', edad=25}
Persona{nombre='Luis', edad=28}
Persona{nombre='Carlos', edad=30}

Lista sin duplicados: 3 personas
Persona{nombre='Ana', edad=25}
Persona{nombre='Carlos', edad=30}
Persona{nombre='Luis', edad=28}

Casos de uso prácticos

Filtrado de valores únicos en una base de datos

Uno de los usos más comunes de distinct() es cuando necesitamos obtener valores únicos de una colección que representa datos de una base de datos:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class DistinctCategoriesExample {
    public static void main(String[] args) {
        // Simulación de productos con categorías
        List<Producto> productos = Arrays.asList(
            new Producto("Laptop", "Electrónica"),
            new Producto("Smartphone", "Electrónica"),
            new Producto("Camiseta", "Ropa"),
            new Producto("Pantalón", "Ropa"),
            new Producto("Tablet", "Electrónica")
        );
        
        // Obtener categorías únicas
        List<String> categoriasUnicas = productos.stream()
                                               .map(Producto::getCategoria)
                                               .distinct()
                                               .collect(Collectors.toList());
        
        System.out.println("Categorías disponibles: " + categoriasUnicas);
    }
    
    static class Producto {
        private String nombre;
        private String categoria;
        
        public Producto(String nombre, String categoria) {
            this.nombre = nombre;
            this.categoria = categoria;
        }
        
        public String getNombre() {
            return nombre;
        }
        
        public String getCategoria() {
            return categoria;
        }
    }
}

La salida sería:

Categorías disponibles: [Electrónica, Ropa]

Eliminación de palabras duplicadas en un texto

Otro caso de uso común es procesar texto para obtener palabras únicas:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class DistinctWordsExample {
    public static void main(String[] args) {
        String texto = "Java es un lenguaje de programación. Java es versátil y potente.";
        
        // Dividir el texto en palabras y eliminar duplicados
        List<String> palabrasUnicas = Arrays.stream(texto.toLowerCase().split("\\W+"))
                                          .filter(palabra -> !palabra.isEmpty())
                                          .distinct()
                                          .collect(Collectors.toList());
        
        System.out.println("Palabras únicas: " + palabrasUnicas);
    }
}

La salida sería:

Palabras únicas: [java, es, un, lenguaje, de, programación, versátil, y, potente]

Consideraciones de rendimiento

Es importante tener en cuenta que distinct() puede tener un impacto en el rendimiento, especialmente con grandes volúmenes de datos, ya que necesita mantener un registro de los elementos ya vistos. Para colecciones muy grandes, podría ser más eficiente usar otras estructuras como Set directamente:

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class DistinctPerformanceExample {
    public static void main(String[] args) {
        List<Integer> numerosGrandes = Arrays.asList(1, 2, 3, 2, 1, 4, 5, 4, 6);
        
        // Usando distinct()
        long tiempoInicio = System.nanoTime();
        long cantidadDistinct = numerosGrandes.stream()
                                           .distinct()
                                           .count();
        long tiempoFin = System.nanoTime();
        System.out.println("Tiempo con distinct(): " + (tiempoFin - tiempoInicio) + " ns");
        
        // Usando HashSet
        tiempoInicio = System.nanoTime();
        Set<Integer> conjuntoNumeros = new HashSet<>(numerosGrandes);
        long cantidadSet = conjuntoNumeros.size();
        tiempoFin = System.nanoTime();
        System.out.println("Tiempo con HashSet: " + (tiempoFin - tiempoInicio) + " ns");
        
        System.out.println("Cantidad de elementos únicos: " + cantidadDistinct);
    }
}

Combinación con otras operaciones

La operación distinct() se puede combinar fácilmente con otras operaciones de Stream para crear flujos de procesamiento más complejos:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class DistinctCombinedExample {
    public static void main(String[] args) {
        List<String> palabras = Arrays.asList("Java", "Python", "C++", "Java", "JavaScript", "Python", "Kotlin");
        
        // Obtener palabras únicas que empiezan con 'J' y ordenarlas
        List<String> resultado = palabras.stream()
                                       .filter(p -> p.startsWith("J"))
                                       .distinct()
                                       .sorted()
                                       .collect(Collectors.toList());
        
        System.out.println("Palabras que empiezan con J (sin duplicados): " + resultado);
    }
}

La salida sería:

Palabras que empiezan con J (sin duplicados): [Java, JavaScript]

La operación distinct() es una herramienta fundamental en la programación funcional con Java que nos permite trabajar con conjuntos de datos únicos de manera elegante y expresiva, mejorando la legibilidad y mantenibilidad de nuestro código.

sorted()

La operación sorted() es una operación intermedia en los Streams de Java que permite ordenar los elementos de un flujo según un criterio específico. Esta funcionalidad es esencial cuando necesitamos procesar datos en un orden determinado antes de realizar operaciones posteriores.

En su forma más básica, sorted() ordena los elementos utilizando su orden natural (implementado a través de la interfaz Comparable). También existe una sobrecarga que acepta un Comparator para definir criterios de ordenación personalizados.

Ordenación natural con sorted()

Para elementos que implementan la interfaz Comparable, podemos usar sorted() sin argumentos:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SortedExample {
    public static void main(String[] args) {
        List<Integer> numeros = Arrays.asList(5, 3, 8, 1, 9, 4, 7);
        
        List<Integer> numerosOrdenados = numeros.stream()
                                              .sorted()
                                              .collect(Collectors.toList());
        
        System.out.println("Lista original: " + numeros);
        System.out.println("Lista ordenada: " + numerosOrdenados);
    }
}

La salida mostrará:

Lista original: [5, 3, 8, 1, 9, 4, 7]
Lista ordenada: [1, 3, 4, 5, 7, 8, 9]

Este método funciona con cualquier tipo que implemente Comparable, como String, Integer, LocalDate, etc.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SortedStringsExample {
    public static void main(String[] args) {
        List<String> nombres = Arrays.asList("Carlos", "Ana", "Zoe", "Miguel", "Elena");
        
        List<String> nombresOrdenados = nombres.stream()
                                             .sorted()
                                             .collect(Collectors.toList());
        
        System.out.println("Nombres ordenados alfabéticamente: " + nombresOrdenados);
    }
}

Resultado:

Nombres ordenados alfabéticamente: [Ana, Carlos, Elena, Miguel, Zoe]

Ordenación personalizada con Comparator

Para criterios de ordenación más complejos o para tipos que no implementan Comparable, podemos usar la versión sobrecargada de sorted() que acepta un Comparator:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class SortedComparatorExample {
    public static void main(String[] args) {
        List<String> nombres = Arrays.asList("Carlos", "Ana", "Zoe", "Miguel", "Elena");
        
        // Ordenar por longitud de cadena (de menor a mayor)
        List<String> porLongitud = nombres.stream()
                                        .sorted(Comparator.comparing(String::length))
                                        .collect(Collectors.toList());
        
        System.out.println("Ordenados por longitud: " + porLongitud);
        
        // Ordenar por longitud de cadena (de mayor a menor)
        List<String> porLongitudInversa = nombres.stream()
                                               .sorted(Comparator.comparing(String::length).reversed())
                                               .collect(Collectors.toList());
        
        System.out.println("Ordenados por longitud inversa: " + porLongitudInversa);
    }
}

Salida:

Ordenados por longitud: [Ana, Zoe, Elena, Carlos, Miguel]
Ordenados por longitud inversa: [Carlos, Miguel, Elena, Ana, Zoe]

Ordenación de objetos personalizados

Para objetos personalizados, es común definir criterios de ordenación específicos:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

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 + ", stock: " + stock + ")";
    }
}

public class SortedProductsExample {
    public static void main(String[] args) {
        List<Producto> productos = Arrays.asList(
            new Producto("Laptop", 999.99, 10),
            new Producto("Smartphone", 599.99, 15),
            new Producto("Tablet", 299.99, 5),
            new Producto("Auriculares", 89.99, 30),
            new Producto("Monitor", 249.99, 8)
        );
        
        // Ordenar por precio (de menor a mayor)
        List<Producto> porPrecio = productos.stream()
                                          .sorted(Comparator.comparing(Producto::getPrecio))
                                          .collect(Collectors.toList());
        
        System.out.println("Productos ordenados por precio:");
        porPrecio.forEach(System.out::println);
        
        // Ordenar por stock (de mayor a menor)
        List<Producto> porStock = productos.stream()
                                         .sorted(Comparator.comparing(Producto::getStock).reversed())
                                         .collect(Collectors.toList());
        
        System.out.println("\nProductos ordenados por stock (mayor a menor):");
        porStock.forEach(System.out::println);
    }
}

Ordenación con múltiples criterios

También podemos combinar varios criterios de ordenación utilizando los métodos thenComparing():

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Empleado {
    private String departamento;
    private String nombre;
    private int edad;
    
    public Empleado(String departamento, String nombre, int edad) {
        this.departamento = departamento;
        this.nombre = nombre;
        this.edad = edad;
    }
    
    public String getDepartamento() { return departamento; }
    public String getNombre() { return nombre; }
    public int getEdad() { return edad; }
    
    @Override
    public String toString() {
        return departamento + " - " + nombre + " (" + edad + " años)";
    }
}

public class MultiCriteriaSortExample {
    public static void main(String[] args) {
        List<Empleado> empleados = Arrays.asList(
            new Empleado("IT", "Carlos", 35),
            new Empleado("RRHH", "Ana", 28),
            new Empleado("IT", "Miguel", 42),
            new Empleado("Marketing", "Elena", 28),
            new Empleado("RRHH", "Luis", 35),
            new Empleado("Marketing", "Sara", 31)
        );
        
        // Ordenar por departamento, luego por edad y finalmente por nombre
        List<Empleado> ordenados = empleados.stream()
            .sorted(
                Comparator.comparing(Empleado::getDepartamento)
                          .thenComparing(Empleado::getEdad)
                          .thenComparing(Empleado::getNombre)
            )
            .collect(Collectors.toList());
        
        System.out.println("Empleados ordenados por departamento, edad y nombre:");
        ordenados.forEach(System.out::println);
    }
}

Salida:

Empleados ordenados por departamento, edad y nombre:
IT - Carlos (35 años)
IT - Miguel (42 años)
Marketing - Elena (28 años)
Marketing - Sara (31 años)
RRHH - Ana (28 años)
RRHH - Luis (35 años)

Combinación con otras operaciones de Stream

La operación sorted() se integra perfectamente con otras operaciones de Stream para crear flujos de procesamiento más complejos:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class SortedCombinedExample {
    public static void main(String[] args) {
        List<String> palabras = Arrays.asList(
            "Java", "Python", "JavaScript", "C#", "Kotlin", "Swift", "Go", "Rust"
        );
        
        // Filtrar palabras con más de 4 letras, ordenarlas por longitud y luego alfabéticamente
        List<String> resultado = palabras.stream()
            .filter(p -> p.length() > 4)
            .sorted(
                Comparator.comparing(String::length)
                          .thenComparing(String::toString)
            )
            .collect(Collectors.toList());
        
        System.out.println("Palabras filtradas y ordenadas: " + resultado);
    }
}

Salida:

Palabras filtradas y ordenadas: [Kotlin, Python, Swift, JavaScript]

Consideraciones de rendimiento

Es importante tener en cuenta que la operación sorted() requiere cargar todos los elementos en memoria para realizar la ordenación, lo que puede afectar al rendimiento con grandes volúmenes de datos. Además, no es una operación paralela eficiente, ya que requiere reunir todos los elementos para ordenarlos.

import java.util.stream.Stream;

public class SortedPerformanceExample {
    public static void main(String[] args) {
        // Crear un stream de 10 millones de números aleatorios
        long inicio = System.currentTimeMillis();
        
        long count = Stream.generate(Math::random)
                         .limit(1_000_000)
                         .sorted()
                         .count();
        
        long fin = System.currentTimeMillis();
        
        System.out.println("Tiempo para ordenar 1 millón de números: " + (fin - inicio) + " ms");
        System.out.println("Cantidad de elementos: " + count);
    }
}

Casos de uso prácticos

Análisis de datos

La ordenación es fundamental en el análisis de datos para identificar patrones, valores extremos o simplemente presentar información de manera organizada:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

class Venta {
    private String producto;
    private double importe;
    private String region;
    
    public Venta(String producto, double importe, String region) {
        this.producto = producto;
        this.importe = importe;
        this.region = region;
    }
    
    public String getProducto() { return producto; }
    public double getImporte() { return importe; }
    public String getRegion() { return region; }
}

public class DataAnalysisExample {
    public static void main(String[] args) {
        List<Venta> ventas = Arrays.asList(
            new Venta("Laptop", 1200, "Norte"),
            new Venta("Smartphone", 800, "Sur"),
            new Venta("Tablet", 500, "Este"),
            new Venta("Laptop", 1100, "Oeste"),
            new Venta("Smartphone", 750, "Norte"),
            new Venta("Monitor", 300, "Sur")
        );
        
        // Agrupar por producto y ordenar por importe total
        Map<String, Double> ventasPorProducto = ventas.stream()
            .collect(Collectors.groupingBy(
                Venta::getProducto,
                Collectors.summingDouble(Venta::getImporte)
            ));
        
        // Ordenar el mapa por valor (importe total)
        ventasPorProducto.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .forEach(entry -> System.out.println(
                entry.getKey() + ": $" + entry.getValue()
            ));
    }
}

Paginación de resultados

En aplicaciones web, es común necesitar ordenar y paginar resultados:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Producto {
    private String nombre;
    private double precio;
    
    public Producto(String nombre, double precio) {
        this.nombre = nombre;
        this.precio = precio;
    }
    
    public String getNombre() { return nombre; }
    public double getPrecio() { return precio; }
    
    @Override
    public String toString() {
        return nombre + " ($" + precio + ")";
    }
}

public class PaginationExample {
    public static void main(String[] args) {
        List<Producto> catalogo = Arrays.asList(
            new Producto("Laptop", 999.99),
            new Producto("Smartphone", 599.99),
            new Producto("Tablet", 299.99),
            new Producto("Auriculares", 89.99),
            new Producto("Monitor", 249.99),
            new Producto("Teclado", 49.99),
            new Producto("Ratón", 29.99),
            new Producto("Impresora", 199.99),
            new Producto("Altavoces", 79.99),
            new Producto("Webcam", 59.99)
        );
        
        // Parámetros de paginación
        int pagina = 2;  // Segunda página
        int elementosPorPagina = 3;
        
        // Ordenar por precio y obtener la página solicitada
        List<Producto> resultadosPaginados = catalogo.stream()
            .sorted(Comparator.comparing(Producto::getPrecio))
            .skip((pagina - 1) * elementosPorPagina)
            .limit(elementosPorPagina)
            .collect(Collectors.toList());
        
        System.out.println("Página " + pagina + " (ordenados por precio):");
        resultadosPaginados.forEach(System.out::println);
    }
}

La operación sorted() es una herramienta fundamental en la programación funcional con Java que nos permite organizar datos de manera flexible y expresiva, facilitando su posterior procesamiento y análisis.

limit(), skip()

Las operaciones limit() y skip() son operaciones intermedias en los Streams de Java que permiten controlar la cantidad de elementos que fluyen a través de un Stream. Estas operaciones son fundamentales cuando necesitamos trabajar con subconjuntos específicos de datos o implementar paginación en nuestras aplicaciones.

Ambas operaciones son complementarias: mientras limit() restringe el número máximo de elementos, skip() omite un número determinado de elementos desde el inicio del Stream.

Funcionamiento básico de limit()

La operación limit() toma un parámetro long que especifica el número máximo de elementos que debe contener el Stream resultante:

Stream<T> limit(long maxSize)

Veamos un ejemplo sencillo:

import java.util.stream.Stream;
import java.util.List;
import java.util.stream.Collectors;

public class LimitExample {
    public static void main(String[] args) {
        // Crear un stream de números del 1 al 10
        Stream<Integer> numeros = Stream.iterate(1, n -> n + 1).limit(10);
        
        List<Integer> primerosCinco = numeros.limit(5)
                                           .collect(Collectors.toList());
        
        System.out.println("Primeros cinco números: " + primerosCinco);
    }
}

La salida sería:

Primeros cinco números: [1, 2, 3, 4, 5]

En este ejemplo, primero creamos un Stream infinito de números enteros comenzando desde 1, pero lo limitamos a 10 elementos. Luego aplicamos limit(5) para obtener solo los primeros 5 elementos.

Funcionamiento básico de skip()

La operación skip() también toma un parámetro long que indica cuántos elementos se deben omitir desde el inicio del Stream:

Stream<T> skip(long n)

Veamos un ejemplo:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SkipExample {
    public static void main(String[] args) {
        List<String> frutas = Arrays.asList("Manzana", "Banana", "Cereza", "Durazno", "Fresa");
        
        List<String> frutasSalteadas = frutas.stream()
                                           .skip(2)
                                           .collect(Collectors.toList());
        
        System.out.println("Frutas originales: " + frutas);
        System.out.println("Frutas después de saltar 2: " + frutasSalteadas);
    }
}

La salida sería:

Frutas originales: [Manzana, Banana, Cereza, Durazno, Fresa]
Frutas después de saltar 2: [Cereza, Durazno, Fresa]

En este caso, skip(2) omite los dos primeros elementos ("Manzana" y "Banana") y devuelve un Stream con los elementos restantes.

Combinando limit() y skip()

Una de las aplicaciones más comunes de estas operaciones es cuando se utilizan juntas para implementar paginación:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class PaginationExample {
    public static void main(String[] args) {
        List<String> elementos = Arrays.asList(
            "Elemento 1", "Elemento 2", "Elemento 3", "Elemento 4", 
            "Elemento 5", "Elemento 6", "Elemento 7", "Elemento 8", 
            "Elemento 9", "Elemento 10", "Elemento 11", "Elemento 12"
        );
        
        int paginaActual = 2;  // Segunda página
        int elementosPorPagina = 4;
        
        List<String> paginaResultado = elementos.stream()
                                             .skip((paginaActual - 1) * elementosPorPagina)
                                             .limit(elementosPorPagina)
                                             .collect(Collectors.toList());
        
        System.out.println("Página " + paginaActual + ": " + paginaResultado);
    }
}

La salida sería:

Página 2: [Elemento 5, Elemento 6, Elemento 7, Elemento 8]

Este patrón es extremadamente útil en aplicaciones web donde necesitamos mostrar resultados paginados.

Casos de uso prácticos

Procesamiento de grandes conjuntos de datos

Cuando trabajamos con grandes volúmenes de datos, podemos usar limit() para procesar solo una muestra:

import java.util.stream.Stream;
import java.util.Random;

public class BigDataSampleExample {
    public static void main(String[] args) {
        Random random = new Random();
        
        // Simular un stream de datos muy grande
        Stream<Integer> datosGrandes = Stream.generate(() -> random.nextInt(1000));
        
        // Procesar solo una muestra de 100 elementos
        double promedio = datosGrandes.limit(100)
                                    .mapToInt(Integer::intValue)
                                    .average()
                                    .orElse(0);
        
        System.out.println("Promedio de la muestra: " + promedio);
    }
}

Implementación de "Top N" resultados

Podemos combinar limit() con otras operaciones como sorted() para obtener los N mejores resultados:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Producto {
    private String nombre;
    private double precio;
    
    public Producto(String nombre, double precio) {
        this.nombre = nombre;
        this.precio = precio;
    }
    
    public String getNombre() { return nombre; }
    public double getPrecio() { return precio; }
    
    @Override
    public String toString() {
        return nombre + " ($" + precio + ")";
    }
}

public class TopNExample {
    public static void main(String[] args) {
        List<Producto> productos = Arrays.asList(
            new Producto("Laptop", 999.99),
            new Producto("Smartphone", 599.99),
            new Producto("Tablet", 299.99),
            new Producto("Auriculares", 89.99),
            new Producto("Monitor", 249.99)
        );
        
        // Obtener los 3 productos más baratos
        List<Producto> top3MasBaratos = productos.stream()
                                              .sorted(Comparator.comparing(Producto::getPrecio))
                                              .limit(3)
                                              .collect(Collectors.toList());
        
        System.out.println("Top 3 productos más baratos:");
        top3MasBaratos.forEach(System.out::println);
    }
}

La salida sería:

Top 3 productos más baratos:
Auriculares ($89.99)
Monitor ($249.99)
Tablet ($299.99)

Omitir elementos no deseados

La operación skip() es útil cuando queremos omitir ciertos elementos, como encabezados en archivos CSV:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class CsvProcessingExample {
    public static void main(String[] args) {
        // Simular líneas de un archivo CSV
        List<String> lineasCsv = Arrays.asList(
            "id,nombre,edad",  // Encabezado
            "1,Ana,28",
            "2,Carlos,35",
            "3,Elena,42",
            "4,Miguel,31"
        );
        
        // Procesar el CSV omitiendo la línea de encabezado
        List<String> datosProcesados = lineasCsv.stream()
                                             .skip(1)  // Omitir encabezado
                                             .map(linea -> {
                                                 String[] partes = linea.split(",");
                                                 return "Usuario: " + partes[1] + " (" + partes[2] + " años)";
                                             })
                                             .collect(Collectors.toList());
        
        datosProcesados.forEach(System.out::println);
    }
}

La salida sería:

Usuario: Ana (28 años)
Usuario: Carlos (35 años)
Usuario: Elena (42 años)
Usuario: Miguel (31 años)

Consideraciones de rendimiento

Tanto limit() como skip() son operaciones de corto circuito, lo que significa que pueden optimizar el procesamiento al evitar operaciones innecesarias:

import java.util.stream.IntStream;

public class ShortCircuitExample {
    public static void main(String[] args) {
        long inicio = System.currentTimeMillis();
        
        // Sin limit(), esto procesaría todos los números hasta 1.000.000
        int suma = IntStream.rangeClosed(1, 1_000_000)
                          .map(n -> {
                              // Simulamos una operación costosa
                              try {
                                  Thread.sleep(1);
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                              return n * 2;
                          })
                          .limit(10)  // Solo necesitamos 10 elementos
                          .sum();
        
        long fin = System.currentTimeMillis();
        
        System.out.println("Suma de los primeros 10 números procesados: " + suma);
        System.out.println("Tiempo de ejecución: " + (fin - inicio) + " ms");
    }
}

En este ejemplo, aunque el Stream contiene un millón de elementos, la operación limit(10) hace que solo se procesen los primeros 10, lo que mejora significativamente el rendimiento.

Comportamiento con Streams paralelos

Cuando trabajamos con Streams paralelos, es importante tener en cuenta que limit() y skip() pueden afectar la paralelización:

import java.util.stream.IntStream;
import java.util.List;
import java.util.stream.Collectors;

public class ParallelStreamExample {
    public static void main(String[] args) {
        // Con un stream secuencial, los resultados son predecibles
        List<Integer> resultadoSecuencial = IntStream.range(0, 20)
                                                  .boxed()
                                                  .skip(5)
                                                  .limit(10)
                                                  .collect(Collectors.toList());
        
        System.out.println("Resultado secuencial: " + resultadoSecuencial);
        
        // Con un stream paralelo, el orden puede variar
        List<Integer> resultadoParalelo = IntStream.range(0, 20)
                                                .parallel()
                                                .boxed()
                                                .skip(5)
                                                .limit(10)
                                                .collect(Collectors.toList());
        
        System.out.println("Resultado paralelo: " + resultadoParalelo);
    }
}

En Streams paralelos, skip() y limit() pueden no comportarse exactamente como esperamos en términos de qué elementos específicos se incluyen, aunque el número total de elementos será correcto.

Aplicaciones en el mundo real

Análisis de datos por lotes

En análisis de datos, a menudo procesamos grandes conjuntos por lotes:

import java.util.stream.Stream;
import java.util.function.Consumer;

public class BatchProcessingExample {
    public static void main(String[] args) {
        // Simulamos un stream de datos grande
        Stream<Integer> datosCompletos = Stream.iterate(1, n -> n + 1).limit(1000);
        
        // Tamaño del lote
        int tamañoLote = 100;
        
        // Procesamos por lotes de 100 elementos
        for (int i = 0; i < 10; i++) {
            int loteActual = i + 1;
            
            Consumer<Integer> procesador = dato -> {
                // Aquí iría la lógica de procesamiento
                // Solo imprimimos para el ejemplo
                if (dato % 50 == 0) {
                    System.out.println("Procesando dato: " + dato);
                }
            };
            
            System.out.println("Procesando lote " + loteActual + "...");
            
            datosCompletos
                .skip(i * tamañoLote)
                .limit(tamañoLote)
                .forEach(procesador);
        }
    }
}

Implementación de API REST con paginación

En aplicaciones web, es común implementar endpoints que devuelven resultados paginados:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

class Usuario {
    private int id;
    private String nombre;
    
    public Usuario(int id, String nombre) {
        this.id = id;
        this.nombre = nombre;
    }
    
    public int getId() { return id; }
    public String getNombre() { return nombre; }
    
    @Override
    public String toString() {
        return "Usuario{id=" + id + ", nombre='" + nombre + "'}";
    }
}

public class RestApiPaginationExample {
    public static void main(String[] args) {
        // Simulamos una base de datos de usuarios
        List<Usuario> todosLosUsuarios = Arrays.asList(
            new Usuario(1, "Ana"),
            new Usuario(2, "Carlos"),
            new Usuario(3, "Elena"),
            new Usuario(4, "Miguel"),
            new Usuario(5, "Laura"),
            new Usuario(6, "Pedro"),
            new Usuario(7, "Sofía"),
            new Usuario(8, "Javier")
        );
        
        // Parámetros de la solicitud
        int pagina = 2;
        int tamaño = 3;
        
        // Procesamiento de la solicitud
        List<Usuario> usuariosPaginados = todosLosUsuarios.stream()
                                                       .skip((pagina - 1) * tamaño)
                                                       .limit(tamaño)
                                                       .collect(Collectors.toList());
        
        // Creamos la respuesta
        Map<String, Object> respuesta = Map.of(
            "pagina", pagina,
            "tamaño", tamaño,
            "total", todosLosUsuarios.size(),
            "datos", usuariosPaginados
        );
        
        // Simulamos la respuesta JSON
        System.out.println("Respuesta API:");
        System.out.println("  pagina: " + respuesta.get("pagina"));
        System.out.println("  tamaño: " + respuesta.get("tamaño"));
        System.out.println("  total: " + respuesta.get("total"));
        System.out.println("  datos: " + respuesta.get("datos"));
    }
}

Las operaciones limit() y skip() son herramientas esenciales en la programación funcional con Java que nos permiten controlar con precisión el flujo de elementos en un Stream, facilitando la implementación de patrones comunes como paginación, muestreo y procesamiento por lotes.

peek()

La operación peek() es una operación intermedia en los Streams de Java que permite inspeccionar elementos mientras fluyen a través de un Stream, sin modificar el Stream en sí. Esta operación es especialmente útil para depuración y logging durante el procesamiento de datos.

A diferencia de otras operaciones intermedias como map() o filter(), peek() no transforma los elementos ni altera el flujo del Stream; simplemente ejecuta una acción sobre cada elemento y luego lo pasa sin cambios al siguiente paso en la cadena de operaciones.

Sintaxis y funcionamiento básico

La operación peek() recibe como parámetro un Consumer<T>, que es una función que acepta un elemento y no devuelve ningún resultado:

Stream<T> peek(Consumer<? super T> action)

Veamos un ejemplo sencillo:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class PeekBasicExample {
    public static void main(String[] args) {
        List<String> nombres = Arrays.asList("Ana", "Carlos", "Elena", "Miguel");
        
        List<String> nombresProcesados = nombres.stream()
            .peek(nombre -> System.out.println("Procesando: " + nombre))
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        
        System.out.println("Resultado final: " + nombresProcesados);
    }
}

La salida sería:

Procesando: Ana
Procesando: Carlos
Procesando: Elena
Procesando: Miguel
Resultado final: [ANA, CARLOS, ELENA, MIGUEL]

En este ejemplo, peek() nos permite ver cada elemento antes de que sea transformado por la operación map().

Uso para depuración

Una de las aplicaciones más comunes de peek() es la depuración de operaciones en cadena:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class PeekDebuggingExample {
    public static void main(String[] args) {
        List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        List<Integer> resultado = numeros.stream()
            .peek(n -> System.out.println("Inicial: " + n))
            .filter(n -> n % 2 == 0)
            .peek(n -> System.out.println("Después de filter: " + n))
            .map(n -> n * n)
            .peek(n -> System.out.println("Después de map: " + n))
            .collect(Collectors.toList());
        
        System.out.println("Resultado final: " + resultado);
    }
}

La salida mostraría el flujo completo de transformaciones:

Inicial: 1
Inicial: 2
Después de filter: 2
Después de map: 4
Inicial: 3
Inicial: 4
Después de filter: 4
Después de map: 16
...
Resultado final: [4, 16, 36, 64, 100]

Este tipo de depuración es invaluable para entender cómo se procesan los datos en cada etapa del Stream.

Verificación de efectos secundarios

También podemos usar peek() para verificar efectos secundarios en objetos mutables:

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class Contador {
    private int valor = 0;
    
    public void incrementar() {
        valor++;
    }
    
    public int getValor() {
        return valor;
    }
}

public class PeekSideEffectExample {
    public static void main(String[] args) {
        List<String> palabras = Arrays.asList("Java", "es", "un", "lenguaje", "de", "programación");
        Contador contador = new Contador();
        
        List<String> palabrasLargas = palabras.stream()
            .filter(p -> p.length() > 3)
            .peek(p -> {
                System.out.println("Palabra larga encontrada: " + p);
                contador.incrementar();
            })
            .collect(Collectors.toList());
        
        System.out.println("Palabras largas: " + palabrasLargas);
        System.out.println("Total de palabras largas: " + contador.getValor());
    }
}

Aunque este ejemplo funciona, es importante recordar que modificar estado externo desde un Stream (como incrementar un contador) no es una práctica recomendada en programación funcional pura. Para contar elementos, sería mejor usar operaciones terminales como count().

Monitoreo de rendimiento

Otra aplicación útil de peek() es el monitoreo de rendimiento:

import java.util.stream.IntStream;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;

public class PeekPerformanceMonitoringExample {
    public static void main(String[] args) {
        IntStream.rangeClosed(1, 5)
            .peek(n -> System.out.println("Iniciando procesamiento de: " + n + " a las " + LocalTime.now()))
            .map(n -> {
                // Simulamos una operación que toma tiempo
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return n * 2;
            })
            .peek(n -> System.out.println("Finalizando procesamiento con resultado: " + n + " a las " + LocalTime.now()))
            .forEach(n -> {});
    }
}

Este código nos permite ver cuánto tiempo toma procesar cada elemento.

Consideraciones importantes

Evaluación perezosa

Es crucial entender que peek(), como todas las operaciones intermedias, está sujeto a la evaluación perezosa. Esto significa que no se ejecutará a menos que haya una operación terminal al final de la cadena:

import java.util.Arrays;
import java.util.List;

public class PeekLazyEvaluationExample {
    public static void main(String[] args) {
        List<String> nombres = Arrays.asList("Ana", "Carlos", "Elena");
        
        // Este peek() nunca se ejecutará porque no hay operación terminal
        nombres.stream()
            .peek(nombre -> System.out.println("Este mensaje no se mostrará: " + nombre))
            .map(String::toUpperCase);
        
        System.out.println("Fin del programa");
    }
}

La salida sería simplemente:

Fin del programa

Para que peek() se ejecute, necesitamos añadir una operación terminal:

nombres.stream()
    .peek(nombre -> System.out.println("Ahora sí se mostrará: " + nombre))
    .map(String::toUpperCase)
    .count();  // Operación terminal

No confundir con forEach()

Es importante no confundir peek() con forEach(). Mientras que peek() es una operación intermedia que devuelve un Stream, forEach() es una operación terminal que consume el Stream:

import java.util.Arrays;
import java.util.List;

public class PeekVsForEachExample {
    public static void main(String[] args) {
        List<String> nombres = Arrays.asList("Ana", "Carlos", "Elena");
        
        // peek() con operación terminal
        System.out.println("Usando peek():");
        nombres.stream()
            .peek(nombre -> System.out.println("  Procesando: " + nombre))
            .count();
        
        // forEach() como operación terminal
        System.out.println("Usando forEach():");
        nombres.stream()
            .forEach(nombre -> System.out.println("  Procesando: " + nombre));
        
        // Esto NO compilará porque forEach() no devuelve un Stream
        // nombres.stream()
        //     .forEach(nombre -> System.out.println("Procesando: " + nombre))
        //     .count();
    }
}

Casos de uso prácticos

Logging en sistemas de producción

En sistemas de producción, peek() puede ser útil para registrar información sin interferir con el flujo de datos:

import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class PeekLoggingExample {
    private static final Logger logger = Logger.getLogger(PeekLoggingExample.class.getName());
    
    public static void main(String[] args) {
        List<String> datos = Arrays.asList("dato1", "dato2", "error", "dato3", "fallo", "dato4");
        
        List<String> procesados = datos.stream()
            .peek(dato -> {
                if (dato.equals("error") || dato.equals("fallo")) {
                    logger.warning("Encontrado dato problemático: " + dato);
                } else {
                    logger.info("Procesando dato normal: " + dato);
                }
            })
            .filter(dato -> !dato.equals("error") && !dato.equals("fallo"))
            .collect(Collectors.toList());
        
        System.out.println("Datos procesados correctamente: " + procesados);
    }
}

Validación de datos

Podemos usar peek() para validar datos antes de procesarlos:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class DatoInvalidoException extends RuntimeException {
    public DatoInvalidoException(String mensaje) {
        super(mensaje);
    }
}

public class PeekValidationExample {
    public static void main(String[] args) {
        List<String> entradas = Arrays.asList("123", "456", "abc", "789", "xyz");
        
        try {
            List<Integer> numeros = entradas.stream()
                .peek(entrada -> {
                    try {
                        Integer.parseInt(entrada);
                    } catch (NumberFormatException e) {
                        throw new DatoInvalidoException("Valor no numérico: " + entrada);
                    }
                })
                .map(Integer::parseInt)
                .collect(Collectors.toList());
            
            System.out.println("Números procesados: " + numeros);
        } catch (DatoInvalidoException e) {
            System.err.println("Error de validación: " + e.getMessage());
        }
    }
}

Análisis estadístico durante el procesamiento

Podemos usar peek() para recopilar estadísticas mientras procesamos datos:

import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;

public class PeekStatisticsExample {
    public static void main(String[] args) {
        List<Double> valores = Arrays.asList(10.5, 20.3, 30.8, 15.2, 25.7);
        DoubleSummaryStatistics stats = new DoubleSummaryStatistics();
        
        List<Double> valoresNormalizados = valores.stream()
            .peek(stats::accept)  // Recopilamos estadísticas
            .map(valor -> valor / stats.getAverage())  // Normalizamos por el promedio
            .collect(Collectors.toList());
        
        System.out.println("Estadísticas: " + stats);
        System.out.println("Valores normalizados: " + valoresNormalizados);
    }
}

La operación peek() es una herramienta valiosa en la programación funcional con Java que nos permite observar y depurar el flujo de datos en un Stream sin alterarlo, facilitando la comprensión y el mantenimiento de nuestro código.

peek()

La operación peek() es una herramienta intermedia en los Streams de Java que permite examinar elementos durante su procesamiento sin alterar el flujo de datos. A diferencia de otras operaciones como map() que transforman elementos, peek() simplemente observa cada elemento, ejecuta una acción sobre él y lo pasa intacto al siguiente paso en la cadena de operaciones.

Esta operación recibe como parámetro un Consumer<T>, que es una función que acepta un elemento y no devuelve ningún resultado:

Stream<T> peek(Consumer<? super T> action)

Visualización del flujo de datos

El uso más común de peek() es para visualizar el estado de los elementos mientras atraviesan un pipeline de Stream:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class PeekVisualizationExample {
    public static void main(String[] args) {
        List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
        
        List<Integer> resultado = numeros.stream()
            .peek(n -> System.out.println("Valor original: " + n))
            .map(n -> n * 2)
            .peek(n -> System.out.println("Después de multiplicar: " + n))
            .filter(n -> n > 5)
            .peek(n -> System.out.println("Después de filtrar: " + n))
            .collect(Collectors.toList());
        
        System.out.println("Resultado final: " + resultado);
    }
}

Este código mostrará cada transformación que sufren los elementos, facilitando la comprensión del flujo de datos:

Valor original: 1
Después de multiplicar: 2
Valor original: 2
Después de multiplicar: 4
Valor original: 3
Después de multiplicar: 6
Después de filtrar: 6
Valor original: 4
Después de multiplicar: 8
Después de filtrar: 8
Valor original: 5
Después de multiplicar: 10
Después de filtrar: 10
Resultado final: [6, 8, 10]

Depuración de operaciones complejas

Cuando trabajamos con operaciones encadenadas complejas, peek() nos ayuda a identificar dónde pueden estar ocurriendo problemas:

import java.util.stream.Stream;
import java.util.List;
import java.util.stream.Collectors;

public class PeekDebuggingComplexExample {
    public static void main(String[] args) {
        try {
            List<Integer> resultado = Stream.of("5", "10", "15", "abc", "20")
                .peek(s -> System.out.println("Procesando string: " + s))
                .map(s -> {
                    try {
                        return Integer.parseInt(s);
                    } catch (NumberFormatException e) {
                        System.err.println("Error al convertir: " + s);
                        throw e;
                    }
                })
                .peek(n -> System.out.println("Número convertido: " + n))
                .filter(n -> n % 2 == 0)
                .peek(n -> System.out.println("Número par encontrado: " + n))
                .collect(Collectors.toList());
            
            System.out.println("Resultado: " + resultado);
        } catch (Exception e) {
            System.err.println("Excepción capturada: " + e.getMessage());
        }
    }
}

Este ejemplo muestra cómo peek() puede ayudarnos a identificar exactamente dónde ocurre un error en el procesamiento.

Medición de rendimiento

Podemos usar peek() para medir el tiempo que toma cada etapa del procesamiento:

import java.util.stream.Stream;
import java.time.Instant;
import java.time.Duration;
import java.util.function.Function;

public class PeekPerformanceMeasurementExample {
    public static void main(String[] args) {
        // Función para medir tiempo
        Function<String, Function<Object, Object>> medirTiempo = etapa -> elemento -> {
            Instant inicio = Instant.now();
            System.out.println("Iniciando " + etapa + " para: " + elemento);
            
            // Simulamos procesamiento
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            Duration duracion = Duration.between(inicio, Instant.now());
            System.out.println("Completado " + etapa + " en: " + duracion.toMillis() + "ms");
            return elemento;
        };
        
        Stream.of(1, 2, 3, 4, 5)
            .peek(e -> medirTiempo.apply("etapa 1").apply(e))
            .map(n -> n * 2)
            .peek(e -> medirTiempo.apply("etapa 2").apply(e))
            .forEach(n -> {});
    }
}

Evaluación perezosa y peek()

Es fundamental entender que peek(), como todas las operaciones intermedias, está sujeto a la evaluación perezosa. Esto significa que el código dentro de peek() no se ejecutará hasta que se encuentre una operación terminal:

import java.util.Arrays;
import java.util.List;

public class PeekLazyExample {
    public static void main(String[] args) {
        List<String> nombres = Arrays.asList("Ana", "Carlos", "Elena");
        
        System.out.println("Antes del stream");
        
        // Este peek() no se ejecutará porque no hay operación terminal
        nombres.stream()
            .peek(nombre -> System.out.println("Procesando: " + nombre));
        
        System.out.println("Después del stream (sin operación terminal)");
        
        // Ahora añadimos una operación terminal
        nombres.stream()
            .peek(nombre -> System.out.println("Ahora sí procesando: " + nombre))
            .count();
        
        System.out.println("Después del stream (con operación terminal)");
    }
}

La salida mostrará que el primer peek() nunca se ejecuta:

Antes del stream
Después del stream (sin operación terminal)
Ahora sí procesando: Ana
Ahora sí procesando: Carlos
Ahora sí procesando: Elena
Después del stream (con operación terminal)

Diferencia entre peek() y forEach()

Es importante distinguir entre peek() (operación intermedia) y forEach() (operación terminal):

import java.util.Arrays;
import java.util.List;

public class PeekVsForEachExample {
    public static void main(String[] args) {
        List<String> frutas = Arrays.asList("Manzana", "Banana", "Cereza");
        
        // peek() permite continuar el procesamiento
        long conteo = frutas.stream()
            .peek(fruta -> System.out.println("Peek: " + fruta))
            .count();
        
        System.out.println("Conteo: " + conteo);
        
        // forEach() termina el stream
        frutas.stream()
            .forEach(fruta -> System.out.println("ForEach: " + fruta));
        
        // Esto NO compilará:
        // frutas.stream()
        //     .forEach(fruta -> System.out.println(fruta))
        //     .count();
    }
}

Casos de uso prácticos

Auditoría de operaciones

peek() es ideal para implementar auditoría sin interferir con la lógica principal:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.time.LocalDateTime;

public class PeekAuditExample {
    public static void main(String[] args) {
        List<String> transacciones = Arrays.asList("TX001", "TX002", "TX003");
        
        List<String> procesadas = transacciones.stream()
            .peek(tx -> registrarAuditoria("INICIO", tx))
            .map(tx -> "Procesado: " + tx)
            .peek(tx -> registrarAuditoria("FIN", tx))
            .collect(Collectors.toList());
        
        System.out.println("Transacciones procesadas: " + procesadas);
    }
    
    private static void registrarAuditoria(String etapa, String datos) {
        System.out.println("[" + LocalDateTime.now() + "] " + etapa + ": " + datos);
    }
}

Validación de datos en tiempo real

Podemos usar peek() para validar datos mientras se procesan:

import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;

public class PeekValidationExample {
    public static void main(String[] args) {
        List<String> datos = Arrays.asList("100", "200", "abc", "300");
        List<String> errores = new ArrayList<>();
        
        List<Integer> validos = datos.stream()
            .peek(valor -> {
                try {
                    Integer.parseInt(valor);
                } catch (NumberFormatException e) {
                    errores.add("Valor inválido: " + valor);
                }
            })
            .filter(valor -> {
                try {
                    Integer.parseInt(valor);
                    return true;
                } catch (NumberFormatException e) {
                    return false;
                }
            })
            .map(Integer::parseInt)
            .collect(Collectors.toList());
        
        System.out.println("Valores procesados: " + validos);
        System.out.println("Errores encontrados: " + errores);
    }
}

Monitoreo de recursos

peek() puede ayudarnos a monitorear el uso de recursos durante el procesamiento:

import java.util.stream.Stream;
import java.util.concurrent.atomic.AtomicLong;

public class PeekResourceMonitoringExample {
    public static void main(String[] args) {
        AtomicLong memoriaUsada = new AtomicLong(0);
        
        Stream.of("dato1", "dato2", "dato3", "dato4", "dato5")
            .peek(dato -> {
                long memoriaActual = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
                memoriaUsada.set(memoriaActual);
                System.out.println("Procesando: " + dato + " - Memoria usada: " + memoriaActual / 1024 + "KB");
            })
            .map(String::toUpperCase)
            .forEach(dato -> {});
        
        System.out.println("Memoria final usada: " + memoriaUsada.get() / 1024 + "KB");
    }
}

Mejores prácticas al usar peek()

  1. Usar peek() solo para observación: No modificar el estado de objetos externos dentro de peek(), ya que esto va contra los principios de la programación funcional.
// Evitar esto:
AtomicInteger contador = new AtomicInteger();
stream.peek(e -> contador.incrementAndGet())...

// Preferir esto:
long contador = stream.count();
  1. Mantener las operaciones ligeras: Las acciones dentro de peek() deben ser rápidas para no afectar el rendimiento.
// Evitar operaciones pesadas
stream.peek(e -> realizarOperacionCostosa(e))...
  1. Desactivar peek() en producción: Considera implementar un mecanismo para desactivar las operaciones de peek() en entornos de producción.
public static <T> Consumer<T> debugPeek(Consumer<T> action) {
    return DEBUG_ENABLED ? action : e -> {};
}

// Uso
stream.peek(debugPeek(e -> System.out.println(e)))...
  1. Combinar con logging estructurado: Para sistemas en producción, integra peek() con sistemas de logging adecuados.
import java.util.logging.Logger;

public class PeekLoggingBestPractice {
    private static final Logger logger = Logger.getLogger(PeekLoggingBestPractice.class.getName());
    
    public static void main(String[] args) {
        Stream.of(1, 2, 3)
            .peek(n -> {
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("Procesando: " + n);
                }
            })
            .forEach(n -> {});
    }
}

La operación peek() es una herramienta valiosa que, cuando se usa correctamente, puede mejorar significativamente la observabilidad y depuración de nuestras aplicaciones basadas en Streams, sin interferir con la lógica principal del procesamiento de datos.

Aprende Java online

Otros ejercicios de programación de Java

Evalúa tus conocimientos de esta lección Transformació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 de distinct() para eliminar elementos duplicados en Streams.
  • Aprender a ordenar elementos con sorted(), incluyendo ordenación natural y personalizada.
  • Manejar la selección y omisión de elementos con limit() y skip(), aplicando paginación y muestreo.
  • Utilizar peek() para inspeccionar y depurar el flujo de datos en Streams sin modificarlo.
  • Reconocer consideraciones de rendimiento y buenas prácticas al usar estas operaciones en flujos de datos grandes.