Guía Introducción a Java Streams

En esta guía vamos a explorar el concepto de Streams en Java, una herramienta poderosa para trabajar con colecciones de datos de manera más eficiente y concisa. Los Streams son una de las características más útiles del lenguaje Java, introducidas en Java 8. Permiten realizar operaciones en secuencias de elementos de manera funcional, sin necesidad de escribir bucles complejos.

¿Qué es un Stream?

Un Stream en Java es una secuencia de elementos que se puede procesar de manera funcional. A diferencia de una colección, que almacena los elementos, un Stream solo los procesa. Esto significa que un Stream no almacena los datos en sí mismo, sino que proporciona una forma de realizar operaciones sobre esos datos.

Características clave de los Streams:

¿Cómo Crear un Stream?

Los Streams se pueden crear a partir de diferentes fuentes, como colecciones, arreglos o generadores. Aquí se muestran algunos ejemplos básicos.

Crear un Stream a partir de una lista

import java.util.List;
import java.util.stream.*;

public class EjemploStream {
    public static void main(String[] args) {
        List<String> nombres = List.of("Ana", "Carlos", "Luis", "Maria");

        // Crear un Stream a partir de la lista
        Stream<String> streamNombres = nombres.stream();
        streamNombres.forEach(System.out::println);  // Imprime cada nombre
    }
}

Crear un Stream a partir de un arreglo

public class EjemploStream {
    public static void main(String[] args) {
        String[] frutas = {"Manzana", "Plátano", "Cereza"};

        // Crear un Stream a partir del arreglo
        Stream<String> streamFrutas = Stream.of(frutas);
        streamFrutas.forEach(System.out::println);  // Imprime cada fruta
    }
}

Crear un Stream a partir de valores generados

public class EjemploStream {
    public static void main(String[] args) {
        // Crear un Stream de números
        Stream<Integer> streamNumeros = Stream.iterate(1, n -> n + 1).limit(5);
        streamNumeros.forEach(System.out::println);  // Imprime 1, 2, 3, 4, 5
    }
}

Operaciones sobre Streams

Existen dos tipos de operaciones que podemos realizar en un Stream:

  1. Operaciones intermedias: Son operaciones que transforman un Stream en otro. Son perezosas, lo que significa que no se ejecutan hasta que se realiza una operación terminal. Ejemplos incluyen filter(), map(), distinct(), etc.
  2. Operaciones terminales: Son operaciones que producen un resultado o efecto secundario, como collect(), forEach(), reduce(), etc.

Ejemplo de operación intermedia: filter()

La operación filter() permite filtrar elementos que cumplen con una condición.

import java.util.List;
import java.util.stream.*;

public class EjemploStream {
    public static void main(String[] args) {
        List<String> nombres = List.of("Ana", "Carlos", "Luis", "Maria");

        // Filtrar los nombres que contienen la letra 'a'
        nombres.stream()
            .filter(nombre -> nombre.contains("a"))
            .forEach(System.out::println);  // Imprime "Ana" y "Maria"
    }
}

Ejemplo de operación intermedia: map()

La operación map() transforma los elementos del Stream aplicando una función.

import java.util.List;
import java.util.stream.*;

public class EjemploStream {
    public static void main(String[] args) {
        List<String> nombres = List.of("Ana", "Carlos", "Luis", "Maria");

        // Convertir los nombres a mayúsculas
        nombres.stream()
            .map(String::toUpperCase)
            .forEach(System.out::println);  // Imprime "ANA", "CARLOS", "LUIS", "MARIA"
    }
}

Ejemplo de operación terminal: collect()

La operación collect() se usa para convertir un Stream en una colección (por ejemplo, una lista o un conjunto).

import java.util.List;
import java.util.stream.*;

public class EjemploStream {
    public static void main(String[] args) {
        List<String> nombres = List.of("Ana", "Carlos", "Luis", "Maria");

        // Crear una lista con los nombres que contienen la letra 'a'
        List<String> nombresFiltrados = nombres.stream()
            .filter(nombre -> nombre.contains("a"))
            .collect(Collectors.toList());

        System.out.println(nombresFiltrados);  // Imprime [Ana, Maria]
    }
}

Ejemplo de operación terminal: reduce()

La operación reduce() se utiliza para combinar los elementos del Stream en un solo valor.

import java.util.List;
import java.util.stream.*;

public class EjemploStream {
    public static void main(String[] args) {
        List<Integer> numeros = List.of(1, 2, 3, 4, 5);

        // Sumar todos los números del Stream
        int suma = numeros.stream()
            .reduce(0, (acumulado, numero) -> acumulado + numero);

        System.out.println(suma);  // Imprime 15
    }
}

Operaciones en Paralelo

Una de las grandes ventajas de los Streams es que pueden ejecutarse de manera paralela. Esto significa que puedes dividir el trabajo de procesamiento entre múltiples hilos para mejorar el rendimiento en colecciones grandes.

import java.util.List;
import java.util.stream.*;

public class EjemploStream {
    public static void main(String[] args) {
        List<String> nombres = List.of("Ana", "Carlos", "Luis", "Maria");

        // Procesar el Stream en paralelo
        nombres.parallelStream()
            .forEach(nombre -> System.out.println(nombre + " - " + Thread.currentThread().getName()));
    }
}

Conclusión

Los Streams en Java proporcionan una forma elegante y eficiente de trabajar con colecciones de datos. Algunas de las operaciones más comunes que se pueden realizar son:

A medida que te familiarices con los Streams, descubrirás que son una herramienta poderosa para escribir código más limpio y eficiente. Te animo a seguir explorando más sobre Streams y su uso en situaciones reales de programación.

Ejercicios Sugeridos

  1. Crea un Stream de números y filtra aquellos que sean pares.
  2. Usa la operación map() para convertir una lista de nombres a su longitud.
  3. Implementa una operación reduce() para encontrar el número máximo en una lista de enteros.
  4. Usa parallelStream() para procesar una lista de cadenas y medir el tiempo de ejecución.

Recuerda que los Streams son una herramienta funcional, por lo que es importante practicar y experimentar para comprender completamente su potencial. ¡Sigue aprendiendo y mejorando tus habilidades en Java!