Creación de Streams

Intermedio
Java
Java
Actualizado: 08/05/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

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.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

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.

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

Completa Java y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración