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

Introducció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:

  1. Fuente: La colección, array u otra fuente de datos de la que se obtienen los elementos.

  2. Operaciones intermedias: Transformaciones que se aplican a los elementos (como filtrado, mapeo, ordenación) y que devuelven un nuevo Stream.

  3. 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.

Aprende Java online

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

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 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.