Métodos de la interface Stream de Java

Los Streams en Java son una forma de procesar y manipular colecciones de elementos de manera eficiente, elegante y en ocasiones paralela. Se pueden utilizar para realizar operaciones de filtrado, mapeo, ordenamiento y reducción, entre otras.

Los Streams ofrecen varias ventajas en comparación con el uso tradicional de bucles como, por ejemplo:

  • Proporcionan una sintaxis más clara y concisa para realizar operaciones en colecciones de elementos.
  • Son más eficientes en el uso de memoria y procesamiento, especialmente en colecciones grandes o cuando se trabajan con grandes cantidades de datos.
  • Permiten el procesamiento en paralelo, lo que puede mejorar significativamente el rendimiento en sistemas con múltiples núcleos de procesamiento.

Para más información les recomiendo leer el post "Programando con streams".

En esta oportunidad vamos a enfocarnos específicamente en las diferentes operaciones que nos ofrece esta interface, viendo ejemplos de cada una.

Las operaciones intermedias, como su nombre indica, son operaciones que se aplican a un Stream y devuelven otro Stream modificado. Se pueden encadenar varias operaciones intermedias para construir un procesamiento complejo del Stream. Algunos ejemplos de operaciones intermedias son filter(), map(), flatMap(), distinct(), sorted(), peek(), entre otros.

Por otro lado, las operaciones terminales son operaciones que se aplican al Stream y producen un resultado final o una acción final, como una colección o un valor primitivo, o la ejecución de una acción, respectivamente. Estas operaciones no pueden ser encadenadas y, cuando se ejecutan, cierran el Stream y terminan el procesamiento del mismo. Algunos ejemplos de operaciones terminales son forEach(), reduce(), collect(), count(), anyMatch(), allMatch(), noneMatch(), findFirst(), findAny(), entre otros.

Es importante tener en cuenta que siempre debe haber al menos una operación terminal para que el procesamiento se realice efectivamente.

 

Operaciones:

 

static <T> Stream<T> iterate(T seed, Predicate<T> hasNext, UnaryOperator<T> next)

Este método acepta tres parámetros:

  1. seed : es el elemento inicial del Stream.
  2. hasNext : es un predicado (una condición) que se aplica a los elementos para determinar cuándo debe terminar la secuencia.
  3. next : es una función que se aplicará al elemento anterior para producir un nuevo elemento.

Es una operación intermedia que nos permite generar un flujo (stream) de elementos a partir de un valor inicial y una función generadora.

import java.util.stream.Stream;
public class Main {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.iterate(1, i -> i <= 20, i -> i * 2);
        stream.forEach(System.out::println);
    }
}
/*Salida
1
2
4
8
16
*/

Stream<T> distinct()

Es una operación intermedia que nos devuelve un nuevo Stream que contiene elementos distintos (únicos), para ello utiliza los métodos equals() y hashCode(). Es decir, elimina los elementos duplicados del Stream original y devuelve un nuevo Stream que contiene solo elementos únicos.

import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<Integer> lista = Arrays.asList(1, 1, 2, 3, 3, 4, 5, 5);
        lista.stream().distinct().forEach(System.out::println);
    }
}
/*Salida:
1
2
3
4
5
*/ 

Stream<T> peek(Consumer<? super T> action)

Es una operación intermedia que nos permite realizar una acción o un efecto secundario en los elementos de un Stream, sin modificarlos. Es útil cuando necesitamos inspeccionar el contenido de un Stream durante el proceso de transformación o de filtrado, para verificar si el resultado es el esperado o para depurar el código.

import java.util.stream.*;
import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<String> lista = Arrays.asList("Juan", "Pedro", "María", "Sofía");
        List<String> nombres = lista.stream()
            .map(String::toUpperCase)
            .peek(System.out::println) // Imprime cada nombre en mayúsculas
            .collect(Collectors.toList());
        System.out.println(nombres);
    }
}
/*Salida:
JUAN
PEDRO
MARÍA
SOFÍA
[JUAN, PEDRO, MARÍA, SOFÍA]
*/

Stream<T> filter(Predicate<? super T> predicate)

Es una operación intermedia que nos devuelve un Stream que contiene sólo los elementos que coinciden con el predicado dado (una condición).

import java.util.stream.*;
import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<Integer> lista = Arrays.asList(3, 4, 6, 12, 20, 100);
        List<Integer> listaNrosDivisiblePor5 = lista.stream().filter(num -> num % 5 == 0).collect(Collectors.toList());
        System.out.println(listaNrosDivisiblePor5); //[20, 100]
    }
}

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper))

Es una operación intermedia que se utiliza para transformar y aplanar un Stream de objetos complejos convirtiéndolo en un Stream plano de objetos simples.

import java.util.stream.*;
import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<List<Integer>> lista = Arrays.asList(
                Arrays.asList(1, 2, 3),
                Arrays.asList(4, 5, 6),
                Arrays.asList(7, 8, 9)
        );
        List<Integer> listaAplanada = lista.stream().flatMap(list -> list.stream()).collect(Collectors.toList());
        System.out.println(lista); //[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
        System.out.println(listaAplanada); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    } 
}   

boolean allMatch(Predicate<? super T> predicate)

Es una operación terminal que nos devuelve true si todos los elementos de la secuencia coinciden con el predicado proporcionado (la condición), caso contrario nos devuelve false. En el caso de que la secuencia esté vacía, se devuelve true y el predicado no se evalúa.

import java.util.*;
public class Main{
    public static void main(String[] args) {
        List<Integer> lista = Arrays.asList(3, 4, 6, 12, 20);
        boolean respuesta = lista.stream().allMatch(n -> n % 3 == 0);
        System.out.println(respuesta); //false
    }
}
import java.util.stream.Stream;
public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Juan", "Pedro", "Gonzalo", "Emiliano");
        boolean respuesta = stream.allMatch(str -> str.length() > 2);
        System.out.println(respuesta); //true
    }
}

boolean anyMatch(Predicate<? super T> predicate)

Es una operación terminal que nos devuelve true si algún elemento de la secuencia coincide con el predicado proporcionado (la condición); de lo contrario false. En el caso de que la secuencia esté vacía, se devuelve false y el predicado no se evalúa.

import java.util.stream.Stream;
import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<Integer> lista = Arrays.asList(3, 4, 6, 12, 20);
        boolean respuesta = lista.stream().anyMatch(n -> (n * (n + 1)) / 4 == 5);
        System.out.println(respuesta); //true
    }
}

Optional<T> findFirst()

Es una operación terminal que devuelve un Optional del primer elemento de esta secuencia, o un Optional vacío si la secuencia está vacía. Si no tiene un orden establecido, puede devolver cualquier elemento que cumpla con las operaciones intermedias.

import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<Integer> lista = Arrays.asList(1, 3, 4, 7, 8, 11, 14, 17);
        Optional<Integer> primerNumeroPar = lista.stream().filter(n -> n % 2 == 0).findFirst();
        if (primerNumeroPar.isPresent()) {
            System.out.println("El primer número par en la lista es: " + primerNumeroPar.get());
        } else {
            System.out.println("No hay números pares en la lista.");
        }
    }
}

<R, A> R collect(Collector<? super T, A, R> collector)

Donde:

  • ‘T’ es el tipo de elemento en la secuencia.
  • ‘A’ es el tipo de acumulador intermedio utilizado para procesar los elementos de la secuencia.
  • ‘R’ es el tipo de resultado final deseado.

Es una operación terminal que se utiliza para indicar el tipo de Collection en la que se devolverá el resultado final de todas las operaciones realizadas sobre el Stream.
La clase Collectors tiene una gama muy amplia de métodos que permiten agrupar elementos de una forma muy variada simplificando el código como, por ejemplo: los métodos <code>toList()</code>, <code>toMap()</code>, y más. Les dejo la documentación de esta clase: Collectors

import java.util.*;
import java.util.stream.*;
public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("agua", "aeropuerto", "barco", "auto", "animal");
        List<String> listaDePalabrasQueEmpiezanConA = stream.filter(s -> s.startsWith("a")).collect(Collectors.toList());
        System.out.println(listaDePalabrasQueEmpiezanConA); // [agua, aeropuerto, auto, animal]
    }
} 

void forEach(Consumer<? super T> action)

Es una operación terminal utilizada para recorrer un Stream de elementos y realizar alguna operación en cada uno de ellos.

El comportamiento de esta operación es explícitamente no determinista, no nos garantiza ningún orden específico en el que los elementos del Stream son procesados.

import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<String> lista = Arrays.asList("Juan", "Pedro",  "Gonzalo", "Emiliano");
        lista.stream().forEach(System.out::println);
    }
}
/*Salida:
Juan
Pedro
Gonzalo
Emiliano
*/  

void forEachOrdered(Consumer<? super T> action)

Al igual que el método forEach(), es una operación terminal utilizada para recorrer un Stream de elementos y realizar alguna operación en cada uno de ellos, pero forEachOrdered() nos asegura que los elementos son procesados en el orden en que aparecen. En otras palabras, ejecuta la acción para un elemento y recién cuando termina, continúa con el siguiente.

import java.util.*;
public class Main {
    public static void main(String[] args) {
        List<Integer> lista = Arrays.asList(10, 19, 20, 1, 2);
        lista.stream().forEachOrdered(System.out::println);
    }
}
/*Salida:
10
19
20
1
2
*/

En resumen:

La interfaz Stream es una herramienta poderosa y útil para el procesamiento de grandes cantidades de datos, y ofrece una mayor facilidad y flexibilidad en el manejo de las colecciones de elementos. Su uso depende de las necesidades específicas del programa y de la complejidad de las operaciones a realizar en los datos.

Mandanos tus sugerencias

Ayudanos con ideas para los artículos de este blog a contacto@somospnt.com

¡Seguínos en nuestras redes sociales para enterarte de los últimos posts!