Java
Tutorial Java: Creación de Streams
Aprende a crear y utilizar Streams en Java con ejemplos claros. Domina métodos básicos para procesamiento funcional y optimizado de datos.
Aprende Java y certifícateIntroducción a Streams en Java
La programación en Java ha evolucionado significativamente a lo largo de los años, incorporando paradigmas que facilitan el desarrollo de aplicaciones más expresivas y mantenibles. Con la llegada de Java 8, se introdujo un concepto revolucionario: los Streams.
Un Stream en Java representa una secuencia de elementos sobre la cual se pueden realizar diversas operaciones. A diferencia de las colecciones tradicionales, los Streams no almacenan datos; en su lugar, transportan valores desde una fuente a través de una pipeline de operaciones. Esta característica fundamental los convierte en una herramienta ideal para el procesamiento de datos de manera declarativa.
Características fundamentales de los Streams
Los Streams en Java poseen varias características que los distinguen de otras estructuras de datos:
No almacenan datos: Un Stream no es una estructura de datos, sino un medio para procesar los elementos contenidos en una colección.
Procesamiento funcional: Permiten expresar operaciones complejas de manera concisa mediante el uso de expresiones lambda.
Evaluación perezosa: Las operaciones en un Stream no se ejecutan hasta que se invoca una operación terminal, lo que permite optimizaciones importantes.
Posible procesamiento paralelo: Los Streams pueden procesar elementos en paralelo sin necesidad de código adicional para la gestión de hilos.
Consumibles: Un Stream solo puede ser recorrido una vez. Una vez utilizado, necesita ser regenerado para volver a procesarlo.
Estructura de operaciones en Streams
Las operaciones con Streams siguen un patrón consistente que se divide en tres partes:
Fuente: La colección, array u otra fuente de datos de la que se obtienen los elementos.
Operaciones intermedias: Transformaciones que se aplican a los elementos (como filtrado, mapeo, ordenación) y que devuelven un nuevo Stream.
Operación terminal: La operación final que produce un resultado o un efecto secundario (como contar, recolectar en una colección, o imprimir).
Veamos un ejemplo básico que ilustra esta estructura:
List<String> nombres = Arrays.asList("Ana", "Juan", "Pedro", "María", "Luis");
long contador = nombres.stream() // Fuente: lista de nombres
.filter(n -> n.length() > 4) // Operación intermedia: filtrado
.count(); // Operación terminal: conteo
System.out.println("Nombres con más de 4 letras: " + contador);
En este ejemplo, creamos un Stream a partir de una lista de nombres, filtramos aquellos con más de 4 letras, y finalmente contamos cuántos cumplen esta condición.
Ventajas de utilizar Streams
El uso de Streams ofrece múltiples beneficios para los desarrolladores Java:
Código más legible: Las operaciones con Streams expresan claramente la intención del programador, haciendo que el código sea más fácil de entender.
Menos propenso a errores: Al reducir la necesidad de bucles explícitos y variables temporales, se minimizan las posibilidades de cometer errores.
Facilita la programación funcional: Los Streams permiten adoptar un estilo de programación más declarativo y funcional.
Mejor rendimiento potencial: Gracias a la evaluación perezosa y la posibilidad de procesamiento paralelo, los Streams pueden ofrecer mejor rendimiento en determinados escenarios.
Streams vs Colecciones
Es importante entender la diferencia conceptual entre Streams y colecciones:
Las colecciones son estructuras de datos que almacenan elementos. El foco está en el almacenamiento y acceso eficiente a los datos.
Los Streams son canales de procesamiento de datos. El foco está en las operaciones que se realizan sobre los datos.
Esta diferencia se ilustra en el siguiente ejemplo:
// Enfoque tradicional con colecciones
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> pares = new ArrayList<>();
for (Integer num : numeros) {
if (num % 2 == 0) {
pares.add(num);
}
}
// Enfoque con Streams
List<Integer> paresConStream = numeros.stream()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList());
Como se puede observar, el enfoque con Streams resulta más conciso y expresivo, eliminando la necesidad de gestionar manualmente la iteración y la acumulación de resultados.
Tipos de operaciones en Streams
Las operaciones que se pueden realizar en un Stream se dividen en dos categorías principales:
Operaciones intermedias: Transforman un Stream en otro Stream. Son perezosas, lo que significa que no se ejecutan hasta que se invoca una operación terminal. Ejemplos incluyen
filter()
,map()
,sorted()
, etc.Operaciones terminales: Producen un resultado o un efecto secundario y cierran el Stream. Ejemplos incluyen
count()
,collect()
,forEach()
, etc.
// Ejemplo de operaciones intermedias y terminales
List<String> ciudades = Arrays.asList("Madrid", "Barcelona", "Valencia", "Sevilla");
String resultado = ciudades.stream()
.filter(c -> c.startsWith("M")) // Operación intermedia
.map(String::toUpperCase) // Operación intermedia
.findFirst() // Operación terminal
.orElse("No encontrada");
System.out.println(resultado); // Imprime: MADRID
En este ejemplo, filter()
y map()
son operaciones intermedias que transforman el Stream, mientras que findFirst()
es una operación terminal que devuelve un Optional
con el primer elemento del Stream (si existe).
Consideraciones al trabajar con Streams
Al utilizar Streams en Java, es importante tener en cuenta algunas consideraciones:
Un Stream no puede ser reutilizado después de una operación terminal.
Las operaciones intermedias son perezosas, lo que significa que no se ejecutan hasta que se invoca una operación terminal.
Los Streams no modifican la fuente de datos original.
Para operaciones que modifican el estado (como ordenar o filtrar), se crea un nuevo Stream con los resultados.
List<Integer> numeros = Arrays.asList(5, 2, 8, 1, 3);
// Esto no modifica la lista original
numeros.stream().sorted().forEach(System.out::println);
// La lista original sigue sin ordenar
System.out.println("Lista original: " + numeros); // [5, 2, 8, 1, 3]
Los Streams representan un cambio de paradigma en la forma de procesar datos en Java, permitiendo un estilo más declarativo y funcional que resulta en código más limpio, mantenible y potencialmente más eficiente.
Métodos básicos para crear Streams
Para trabajar con Streams en Java, el primer paso es crear uno. Java proporciona diversos métodos para generar Streams a partir de diferentes fuentes de datos. Conocer estas opciones es fundamental para aprovechar al máximo la API de Streams.
Creación desde colecciones
La forma más común de crear un Stream es a partir de una colección existente. Todas las clases que implementan la interfaz Collection
en Java disponen del método stream()
que devuelve un Stream con los elementos de la colección.
// Creación de Stream a partir de una List
List<String> lenguajes = Arrays.asList("Java", "Python", "JavaScript", "C#");
Stream<String> streamLenguajes = lenguajes.stream();
// Creación de Stream a partir de un Set
Set<Integer> numeros = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
Stream<Integer> streamNumeros = numeros.stream();
Este método es particularmente útil cuando ya tenemos los datos organizados en una colección y queremos procesarlos utilizando las operaciones de la API de Streams.
Creación desde arrays
Para crear un Stream a partir de un array, Java proporciona la clase utilitaria Arrays
con el método estático stream()
:
// Stream a partir de un array de String
String[] nombres = {"Ana", "Carlos", "Lucía", "Miguel"};
Stream<String> streamNombres = Arrays.stream(nombres);
// Stream a partir de un array de primitivos (int)
int[] valores = {10, 20, 30, 40, 50};
IntStream streamValores = Arrays.stream(valores);
Cuando trabajamos con arrays de tipos primitivos, Java proporciona implementaciones especializadas como IntStream
, LongStream
y DoubleStream
que ofrecen métodos optimizados para estos tipos.
Creación con Stream.of()
El método estático of()
de la interfaz Stream
permite crear un Stream directamente a partir de valores individuales:
// Stream a partir de valores individuales
Stream<String> streamColores = Stream.of("Rojo", "Verde", "Azul");
// Stream a partir de un array usando Stream.of()
Integer[] numeros = {1, 2, 3, 4, 5};
Stream<Integer> streamNumeros = Stream.of(numeros);
Este método es especialmente útil cuando queremos crear un Stream con un número pequeño de elementos conocidos, sin necesidad de crear primero una colección.
Streams vacíos
En ocasiones, necesitamos crear un Stream vacío como caso base o para representar la ausencia de elementos:
// Creación de un Stream vacío
Stream<String> streamVacio = Stream.empty();
Los Streams vacíos son útiles en situaciones donde podríamos devolver null
, pero preferimos seguir el patrón de retornar un objeto válido que representa "ningún elemento".
Streams a partir de un String
Los objetos String
pueden convertirse en Streams de caracteres utilizando el método chars()
:
// Stream de caracteres a partir de un String
String texto = "Hola";
IntStream streamChars = texto.chars();
// Imprime los códigos ASCII: 72, 111, 108, 97
streamChars.forEach(System.out::println);
Este método devuelve un IntStream
con los valores Unicode de cada carácter, lo que puede ser útil para operaciones de procesamiento de texto a nivel de carácter.
Creación desde archivos
La API de Java NIO proporciona métodos para crear Streams a partir de líneas de texto en archivos:
try {
// Stream de líneas de un archivo
Stream<String> lineas = Files.lines(Paths.get("archivo.txt"));
lineas.forEach(System.out::println);
lineas.close(); // Importante cerrar el Stream para liberar recursos
} catch (IOException e) {
e.printStackTrace();
}
Este enfoque facilita enormemente el procesamiento de archivos de texto, permitiendo aplicar operaciones de Stream directamente a las líneas leídas.
Creación desde BufferedReader
La clase BufferedReader
también ofrece un método lines()
que devuelve un Stream con las líneas de texto:
try (BufferedReader reader = new BufferedReader(new FileReader("datos.txt"))) {
// Stream de líneas desde un BufferedReader
Stream<String> lineas = reader.lines();
lineas.forEach(System.out::println);
// No es necesario cerrar el Stream explícitamente cuando usamos try-with-resources
} catch (IOException e) {
e.printStackTrace();
}
Esta opción es útil cuando ya tenemos un BufferedReader
abierto y queremos procesar su contenido de manera funcional.
Streams a partir de un rango de números
Para crear Streams de números en un rango específico, las interfaces especializadas IntStream
, LongStream
y DoubleStream
proporcionan métodos útiles:
// Stream de enteros del 1 al 5 (inclusive)
IntStream streamRango = IntStream.rangeClosed(1, 5);
// Imprime: 1, 2, 3, 4, 5
streamRango.forEach(n -> System.out.print(n + ", "));
// Stream de enteros del 1 al 4 (excluyendo el 5)
IntStream streamRangoExclusivo = IntStream.range(1, 5);
// Imprime: 1, 2, 3, 4
streamRangoExclusivo.forEach(n -> System.out.print(n + ", "));
Estos métodos son particularmente útiles para generar secuencias numéricas sin necesidad de crear y poblar colecciones previamente.
Combinación de Streams
También es posible combinar varios Streams existentes en uno solo mediante el método concat()
:
Stream<String> stream1 = Stream.of("A", "B", "C");
Stream<String> stream2 = Stream.of("X", "Y", "Z");
// Combinación de dos Streams
Stream<String> streamCombinado = Stream.concat(stream1, stream2);
// Imprime: A, B, C, X, Y, Z
streamCombinado.forEach(s -> System.out.print(s + ", "));
Esta técnica es útil cuando tenemos datos provenientes de diferentes fuentes y queremos procesarlos de manera uniforme.
Streams a partir de un Spliterator
Para casos más avanzados, podemos crear Streams utilizando un Spliterator
, que permite un control más fino sobre cómo se dividen y procesan los elementos:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
Spliterator<Integer> spliterator = numeros.spliterator();
Stream<Integer> streamDesdeSpliterator = StreamSupport.stream(spliterator, false);
El segundo parámetro del método stream()
indica si el Stream debe ser paralelo (true
) o secuencial (false
).
Consideraciones prácticas
Al crear Streams, es importante tener en cuenta algunas consideraciones:
- Los Streams deben cerrarse cuando provienen de recursos externos (como archivos) para evitar fugas de recursos.
- Un Stream solo puede consumirse una vez; después de una operación terminal, intentar usar el mismo Stream resultará en una excepción.
- Para operaciones que requieren múltiples pasadas sobre los datos, es mejor mantener la colección original y crear nuevos Streams según sea necesario.
List<String> palabras = Arrays.asList("Java", "Stream", "API");
// Crear múltiples Streams a partir de la misma colección
long contador = palabras.stream().filter(p -> p.length() > 4).count();
List<String> mayusculas = palabras.stream().map(String::toUpperCase).collect(Collectors.toList());
Dominar los diferentes métodos para crear Streams es el primer paso para aprovechar eficazmente la API de Streams de Java, permitiéndonos escribir código más conciso, expresivo y funcional para el procesamiento de datos.
Otros ejercicios de programación de Java
Evalúa tus conocimientos de esta lección Creación de Streams con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
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
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 qué es un Stream en Java y sus características fundamentales.
- Identificar la diferencia entre Streams y colecciones.
- Aprender a crear Streams desde colecciones, arrays, valores individuales, archivos y otras fuentes.
- Conocer las operaciones intermedias y terminales en Streams.
- Aplicar buenas prácticas y consideraciones al trabajar con Streams en Java.