Guía Completa sobre la API de Streams de Java
Introducción
La Stream API de Java, introducida en Java 8, permite procesar colecciones de datos (como listas, conjuntos, etc.) de manera declarativa. En lugar de iterar manualmente sobre una colección, puedes usar streams para trabajar con los elementos de forma más clara, concisa y eficiente.
En esta guía, exploraremos los conceptos fundamentales de la API de Streams, proporcionando ejemplos claros y ejercicios prácticos para ayudarte a comprender y utilizar esta poderosa herramienta.
1. ¿Qué es un Stream?
Un Stream es una secuencia de elementos que puede ser procesada de manera secuencial o paralela. A diferencia de las colecciones tradicionales (listas, conjuntos), un stream no almacena datos, sino que los procesa. El propósito de un stream es permitir operaciones de manera fluida y eficiente sobre los datos.
Características de un Stream:
- No almacena datos: Es solo una secuencia que permite operaciones sobre los datos.
- Operaciones de solo lectura: Un stream no modifica la fuente de datos original.
- Lazy Evaluation (Evaluación perezosa): Las operaciones en un stream no se ejecutan hasta que se invoca una operación terminal.
2. Crear un Stream
En Java, puedes crear un Stream de varias maneras:
2.1 Crear un Stream a partir de una colección
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
// Crear un Stream
numeros.stream()
.forEach(System.out::println); // Imprime los elementos
}
}
2.2 Crear un Stream a partir de un array
int[] numeros = {1, 2, 3, 4, 5};
Arrays.stream(numeros)
.forEach(System.out::println);
2.3 Crear un Stream utilizando el método Stream.of()
Stream<String> nombres = Stream.of("Juan", "Maria", "Pedro");
nombres.forEach(System.out::println);
3. Operaciones Intermedias
Las operaciones intermedias transforman un Stream, pero no lo consumen. Estas operaciones son perezosas, es decir, no se ejecutan hasta que se invoque una operación terminal. Algunos ejemplos comunes de operaciones intermedias son:
3.1 filter()
: Filtrar elementos
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6);
numeros.stream()
.filter(n -> n % 2 == 0) // Filtra los números pares
.forEach(System.out::println); // Imprime: 2, 4, 6
3.2 map()
: Transformar elementos
List<String> palabras = Arrays.asList("java", "stream", "api");
palabras.stream()
.map(String::toUpperCase) // Convierte cada palabra a mayúsculas
.forEach(System.out::println); // Imprime: JAVA, STREAM, API
3.3 distinct()
: Eliminar duplicados
List<Integer> numeros = Arrays.asList(1, 2, 3, 3, 4, 4, 5);
numeros.stream()
.distinct() // Elimina los duplicados
.forEach(System.out::println); // Imprime: 1, 2, 3, 4, 5
3.4 sorted()
: Ordenar elementos
List<Integer> numeros = Arrays.asList(5, 1, 4, 2, 3);
numeros.stream()
.sorted() // Ordena de menor a mayor
.forEach(System.out::println); // Imprime: 1, 2, 3, 4, 5
3.5 limit()
: Limitar el número de elementos
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
numeros.stream()
.limit(3) // Toma solo los primeros 3 elementos
.forEach(System.out::println); // Imprime: 1, 2, 3
4. Operaciones Terminales
Las operaciones terminales son aquellas que consumen el Stream y producen un resultado (como una colección, un valor agregado, etc.). A continuación, exploraremos algunas operaciones terminales comunes:
4.1 forEach()
: Ejecutar una acción sobre cada elemento
List<String> palabras = Arrays.asList("java", "stream", "api");
palabras.stream()
.forEach(System.out::println); // Imprime: java, stream, api
4.2 collect()
: Recoger los elementos en una colección
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> cuadrados = numeros.stream()
.map(n -> n * n)
.collect(Collectors.toList()); // Recoge los elementos en una lista
System.out.println(cuadrados); // Imprime: [1, 4, 9, 16, 25]
4.3 reduce()
: Reducir los elementos a un único valor
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
int suma = numeros.stream()
.reduce(0, Integer::sum); // Suma los números
System.out.println(suma); // Imprime: 15
4.4 anyMatch()
, allMatch()
, noneMatch()
: Comprobaciones booleanas
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
boolean hayPares = numeros.stream()
.anyMatch(n -> n % 2 == 0); // Comprueba si hay algún número par
System.out.println(hayPares); // Imprime: true
5. Streams Paralelos
La Stream API de Java también permite trabajar con streams paralelos. Esto puede ser útil para mejorar el rendimiento cuando se trabaja con grandes volúmenes de datos, ya que distribuye el procesamiento a varios núcleos de la CPU.
Para convertir un stream en paralelo, solo necesitas invocar el método parallel()
:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
numeros.parallelStream()
.forEach(n -> System.out.println(Thread.currentThread().getName() + ": " + n));
Este código distribuirá los elementos de numeros
entre diferentes hilos.
6. Ejercicios Prácticos
A continuación, se presentan algunos ejercicios para poner a prueba lo aprendido. No los resolveremos aquí, pero puedes intentar implementarlos por tu cuenta.
- Filtrar números mayores que 10: Dada una lista de números, crea un stream que filtre aquellos mayores que 10 y los imprima.
- Sumar los cuadrados de los números: Dada una lista de números, crea un stream que calcule la suma de los cuadrados de cada número.
- Comprobar si todos los números son positivos: Dada una lista de números, crea un stream que determine si todos los números son positivos.
- Obtener el primer número impar: Dada una lista de números, crea un stream que encuentre el primer número impar.
7. Conclusión
La API de Streams de Java es una herramienta poderosa para procesar colecciones de manera más eficiente y legible. A través de operaciones intermedias y terminales, puedes transformar y analizar datos de forma declarativa y fluida.
Resumen:
- Streams permiten trabajar con colecciones de datos de manera funcional.
- Operaciones intermedias como
filter()
,map()
, ysorted()
transforman los datos sin consumir el stream. - Operaciones terminales como
forEach()
,collect()
, yreduce()
producen resultados finales. - Los streams paralelos pueden mejorar el rendimiento al distribuir el procesamiento entre varios núcleos.
Te animamos a continuar practicando con los ejercicios propuestos y explorar más sobre el uso de streams en situaciones del mundo real. ¡La API de Streams es una herramienta esencial para cualquier desarrollador Java!