Bienvenido al tutorial de Java 8 Stream API. En las últimas publicaciones sobre Java 8, examinamos los Cambios en la Interfaz de Java 8 y las Interfaces Funcionales y Expresiones Lambda. Hoy nos centraremos en una de las principales API introducidas en Java 8: Java Stream.
Java 8 Stream
- Java 8 Stream
- Colecciones y Java Stream
- Interfaces Funcionales en Java 8 Stream
- java.util.Optional
- java.util.Spliterator
- Operaciones intermedias y terminales del flujo de Java
- Operaciones de detección de cortocircuito del flujo de Java
- Ejemplos de flujo de Java
- Limitaciones del API de Java 8 Stream
Flujo de Java
Antes de examinar los ejemplos del API de Java Stream, veamos por qué fue necesario. Supongamos que queremos iterar sobre una lista de enteros y encontrar la suma de todos los enteros mayores que 10. Antes de Java 8, el enfoque para hacerlo sería:
private static int sumIterator(List<Integer> list) {
Iterator<Integer> it = list.iterator();
int sum = 0;
while (it.hasNext()) {
int num = it.next();
if (num > 10) {
sum += num;
}
}
return sum;
}
Hay tres problemas principales con el enfoque anterior:
- Solo queremos saber la suma de enteros pero también tendríamos que proporcionar cómo se llevará a cabo la iteración, esto también se llama iteración externa porque el programa cliente maneja el algoritmo para iterar sobre la lista.
- El programa es secuencial por naturaleza, no hay forma de hacer esto en paralelo fácilmente.
- Hay mucho código incluso para una tarea simple.
Para superar todas las deficiencias anteriores, se introdujo la API de Java 8 Stream. Podemos usar la API de Stream de Java para implementar la iteración interna, que es mejor porque el marco de trabajo de Java está controlando la iteración. La iteración interna proporciona varias características como ejecución secuencial y paralela, filtrado basado en los criterios dados, mapeo, etc. La mayoría de los argumentos de los métodos de la API de Stream de Java 8 son interfaces funcionales, por lo que las expresiones lambda funcionan muy bien con ellos. Veamos cómo podemos escribir la lógica anterior en una declaración de una sola línea usando Streams de Java.
private static int sumStream(List<Integer> list) {
return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
Observe que el programa anterior utiliza la estrategia de iteración del marco de Java, métodos de filtrado y mapeo, y aumentaría la eficiencia. En primer lugar, veremos los conceptos básicos de la API de Streams de Java 8 y luego pasaremos por algunos ejemplos para comprender los métodos más comúnmente utilizados.
Colecciones y Java Stream
A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a java Stream is a data structure that is computed on-demand. Java Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations. Such as we can create a stream from the list and filter it based on a condition. Java Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expression. As you can see in the above example that using lambda expressions make our code readable and short. Java 8 Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization. Java Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times. Java 8 Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections. All the Java Stream API interfaces and classes are in the java.util.stream
package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for primitive types – IntStream
, LongStream
and DoubleStream
.
Interfaces Funcionales en Java 8 Stream
Algunas de las interfaces funcionales comúnmente utilizadas en los métodos de API de Stream de Java 8 son:
- Function y BiFunction: Function representa una función que toma un tipo de argumento y devuelve otro tipo de argumento.
Function<T, R>
es la forma genérica donde T es el tipo de entrada de la función y R es el tipo del resultado de la función. Para manejar tipos primitivos, existen interfaces Function específicas:ToIntFunction
,ToLongFunction
,ToDoubleFunction
,ToIntBiFunction
,ToLongBiFunction
,ToDoubleBiFunction
,LongToIntFunction
,LongToDoubleFunction
,IntToLongFunction
,IntToDoubleFunction
, etc. Algunos de los métodos de Stream donde se usaFunction
o su especialización primitiva son:- <R> Stream<R> map(Function<? super T, ? extends R> mapper)
- IntStream mapToInt(ToIntFunction<? super T> mapper) – de manera similar para flujos específicos de devolución de primitivos long y double.
- IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – de manera similar para long y double
- <A> A[] toArray(IntFunction<A[]> generator)
- <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- Predicate y BiPredicate: Representa un predicado contra el cual se prueban los elementos del flujo. Esto se usa para filtrar elementos del flujo de Java. Al igual que
Function
, existen interfaces específicas de primitivas para int, long y double. Algunos de los métodos de Stream donde se usan especializaciones dePredicate
oBiPredicate
son:- Stream<T> filter(Predicate<? super T> predicate)
- boolean anyMatch(Predicate<? super T> predicate)
- boolean allMatch(Predicate<? super T> predicate)
- boolean noneMatch(Predicate<? super T> predicate)
- Consumer y BiConsumer: Representa una operación que acepta un único argumento de entrada y no devuelve ningún resultado. Se puede utilizar para realizar alguna acción en todos los elementos del flujo de Java. Algunos de los métodos de Stream de Java 8 donde se utilizan las interfaces
Consumer
,BiConsumer
o sus especializaciones primitivas son:- Stream<T> peek(Consumer<? super T> acción)
- void forEach(Consumer<? super T> acción)
- void forEachOrdered(Consumer<? super T> acción)
- Supplier: Supplier representa una operación mediante la cual podemos generar nuevos valores en el flujo. Algunos de los métodos en Stream que toman argumentos de tipo
Supplier
son:- public static<T> Stream<T> generate(Supplier<T> s)
- <R> R collect(Supplier<R> proveedor, BiConsumer<R, ? super T> acumulador, BiConsumer<R, R> combinador)
java.util.Optional
Java Optional es un objeto contenedor que puede o no contener un valor no nulo. Si un valor está presente, isPresent()
devolverá verdadero y get()
devolverá el valor. Las operaciones terminales de Stream devuelven un objeto Optional. Algunos de estos métodos son:
- Optional<T> reduce(BinaryOperator<T> acumulador)
- Optional<T> min(Comparator<? super T> comparador)
- Optional<T> max(Comparator<? super T> comparador)
- Optional<T> findFirst()
- Optional<T> findAny()
java.util.Spliterator
Para admitir la ejecución paralela en Java 8 Stream API, se utiliza la interfaz Spliterator
. El método trySplit
de Spliterator devuelve un nuevo Spliterator que gestiona un subconjunto de los elementos del Spliterator original.
Operaciones Intermedias y Terminales de Java Stream
Las operaciones de Java Stream API que devuelven un nuevo Stream se llaman operaciones intermedias. La mayoría de las veces, estas operaciones son perezosas por naturaleza, por lo que comienzan a producir nuevos elementos de flujo y los envían a la siguiente operación. Las operaciones intermedias nunca son operaciones que producen el resultado final. Las operaciones intermedias comúnmente utilizadas son filter
y map
. Las operaciones de Java 8 Stream API que devuelven un resultado o producen un efecto secundario. Una vez que se llama al método terminal en un flujo, consume el flujo y después de eso no podemos usar el flujo. Las operaciones terminales son ávidas por naturaleza, es decir, procesan todos los elementos en el flujo antes de devolver el resultado. Los métodos terminales comúnmente utilizados son forEach
, toArray
, min
, max
, findFirst
, anyMatch
, allMatch
, etc. Puede identificar los métodos terminales por el tipo de retorno, nunca devolverán un Stream.
Operaciones de Cortocircuito de Java Stream
Una operación intermedia se llama de cortocircuito si puede producir un flujo finito para un flujo infinito. Por ejemplo, limit()
y skip()
son dos operaciones intermedias de cortocircuito. Una operación terminal se llama de cortocircuito si puede terminar en tiempo finito para un flujo infinito. Por ejemplo, anyMatch
, allMatch
, noneMatch
, findFirst
y findAny
son operaciones terminales de cortocircuito.
Ejemplos de Java Stream
I have covered almost all the important parts of the Java 8 Stream API. It’s exciting to use this new API features and let’s see it in action with some java stream examples.
Creación de Java Streams
Existen varias formas de crear un flujo de Java a partir de matrices y colecciones. Veamos esto con ejemplos simples.
-
Podemos usar
Stream.of()
para crear un flujo a partir de un tipo similar de datos. Por ejemplo, podemos crear un flujo de Java de enteros a partir de un grupo de objetos int o Integer.Stream<Integer> stream = Stream.of(1,2,3,4);
-
Podemos usar
Stream.of()
con un array de objetos para devolver el flujo. Tenga en cuenta que no admite autoboxing, por lo que no podemos pasar un array de tipo primitivo.Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); // funciona bien Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); // Error en tiempo de compilación, Incompatibilidad de tipos: no se puede convertir de Stream<int[]> a Stream<Integer>
-
Podemos usar la colección
stream()
para crear un flujo secuencial yparallelStream()
para crear un flujo paralelo.List<Integer> myList = new ArrayList<>(); for(int i=0; i<100; i++) myList.add(i); //flujo secuencial Stream<Integer> sequentialStream = myList.stream(); //flujo paralelo Stream<Integer> parallelStream = myList.parallelStream();
-
Podemos usar los métodos
Stream.generate()
yStream.iterate()
para crear un Stream.Stream<String> stream1 = Stream.generate(() -> {return "abc";}); Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
-
Usando los métodos
Arrays.stream()
yString.chars()
.LongStream is = Arrays.stream(new long[]{1,2,3,4}); IntStream is2 = "abc".chars();
Convirtiendo un Stream de Java a una Colección o Array
Existen varias formas de obtener una Colección o Array a partir de un Stream de Java.
-
Podemos usar el método
collect()
de Java Stream para obtener List, Map o Set a partir del flujo.Stream<Integer> intStream = Stream.of(1,2,3,4); List<Integer> intList = intStream.collect(Collectors.toList()); System.out.println(intList); //imprime [1, 2, 3, 4] intStream = Stream.of(1,2,3,4); //el flujo está cerrado, así que necesitamos crearlo de nuevo Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10)); System.out.println(intMap); //imprime {1=11, 2=12, 3=13, 4=14}
-
Podemos usar el método
toArray()
del flujo para crear un array a partir del mismo.Stream<Integer> intStream = Stream.of(1,2,3,4); Integer[] intArray = intStream.toArray(Integer[]::new); System.out.println(Arrays.toString(intArray)); //imprime [1, 2, 3, 4]
Operaciones Intermedias de Java Stream
Vamos a ver un ejemplo de operaciones intermedias de flujo de Java Stream comúnmente utilizado.
-
Ejemplo de filtro de Stream(): Podemos usar el método filter() para probar los elementos del stream según una condición y generar una lista filtrada.
List<Integer> myList = new ArrayList<>(); for(int i=0; i<100; i++) myList.add(i); Stream<Integer> sequentialStream = myList.stream(); Stream<Integer> highNums = sequentialStream.filter(p -> p > 90); // filtrar números mayores que 90 System.out.print("Números altos mayores que 90="); highNums.forEach(p -> System.out.print(p+" ")); // imprime "Números altos mayores que 90=91 92 93 94 95 96 97 98 99 "
-
Ejemplo de map() en Stream: Podemos usar map() para aplicar funciones a un stream. Veamos cómo podemos usarlo para aplicar la función de mayúsculas a una lista de cadenas.
Stream<String> nombres = Stream.of("aBc", "d", "ef"); System.out.println(nombres.map(s -> { return s.toUpperCase(); }).collect(Collectors.toList())); // imprime [ABC, D, EF]
-
Ejemplo de sorted() en Stream: Podemos usar sorted() para ordenar los elementos del stream pasando un argumento Comparator.
Stream<String> nombres2 = Stream.of("aBc", "d", "ef", "123456"); List<String> ordenadoAlReves = nombres2.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); System.out.println(ordenadoAlReves); // [ef, d, aBc, 123456] Stream<String> nombres3 = Stream.of("aBc", "d", "ef", "123456"); List<String> ordenNatural = nombres3.sorted().collect(Collectors.toList()); System.out.println(ordenNatural); //[123456, aBc, d, ef]
-
Ejemplo de flatMap() en Stream: Podemos usar flatMap() para crear un stream a partir del stream de listas. Veamos un ejemplo simple para aclarar esta duda.
Stream<List<String>> listaOriginalNombres = Stream.of( Arrays.asList("Pankaj"), Arrays.asList("David", "Lisa"), Arrays.asList("Amit")); // aplanar el stream de List<String> a un stream de String Stream<String> flatStream = listaOriginalNombres .flatMap(listaStr -> listaStr.stream()); flatStream.forEach(System.out::println);
Operaciones terminales de Java Stream
Vamos a ver algunos ejemplos de operaciones terminales de flujo de Java.
-
Ejemplo de reduce() de Stream: Podemos usar reduce() para realizar una reducción en los elementos del flujo, utilizando una función de acumulación asociativa, y devolver un Optional. Veamos cómo podemos usarlo para multiplicar los enteros en un flujo.
Stream<Integer> numbers = Stream.of(1,2,3,4,5); Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;}); if(intOptional.isPresent()) System.out.println("Multiplicación = "+intOptional.get()); //120
-
Ejemplo de count() de Stream: Podemos usar esta operación terminal para contar el número de elementos en el flujo.
Stream<Integer> numbers1 = Stream.of(1,2,3,4,5); System.out.println("Número de elementos en el flujo="+numbers1.count()); //5
-
Ejemplo de forEach() en Stream: Esto se puede utilizar para iterar sobre el stream. Podemos usar esto en lugar del iterador. Veamos cómo usarlo para imprimir todos los elementos del stream.
Stream<Integer> numbers2 = Stream.of(1,2,3,4,5); numbers2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
-
Ejemplos de match() en Stream: Veamos algunos ejemplos de métodos de coincidencia en la API de Stream.
Stream<Integer> numbers3 = Stream.of(1,2,3,4,5); System.out.println("¿El Stream contiene 4? "+numbers3.anyMatch(i -> i==4)); //¿El Stream contiene 4? true Stream<Integer> numbers4 = Stream.of(1,2,3,4,5); System.out.println("¿El Stream contiene todos los elementos menores que 10? "+numbers4.allMatch(i -> i<10)); //¿El Stream contiene todos los elementos menores que 10? true Stream<Integer> numbers5 = Stream.of(1,2,3,4,5); System.out.println("¿El Stream no contiene 10? "+numbers5.noneMatch(i -> i==10)); //¿El Stream no contiene 10? true
-
Ejemplo de findFirst() en Stream: Esta es una operación terminal de cortocircuito, veamos cómo podemos usarla para encontrar la primera cadena de un stream que comience con D.
Stream<String> nombres4 = Stream.of("Pankaj", "Amit", "David", "Lisa"); Optional<String> primerNombreConD = nombres4.filter(i -> i.startsWith("D")).findFirst(); if(primerNombreConD.isPresent()){ System.out.println("Primer nombre que comienza con D="+primerNombreConD.get()); //David }
Limitaciones de la API de Stream de Java 8
La API de Stream de Java 8 aporta muchas novedades para trabajar con listas y arrays, pero también tiene algunas limitaciones.
-
Expresiones lambda sin estado: Si estás utilizando un flujo paralelo y las expresiones lambda tienen estado, puede resultar en respuestas aleatorias. Veámoslo con un programa simple.
StatefulParallelStream.java
package com.journaldev.java8.stream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class StatefulParallelStream { public static void main(String[] args) { List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); List<Integer> result = new ArrayList<Integer>(); Stream<Integer> stream = ss.parallelStream(); stream.map(s -> { synchronized (result) { if (result.size() < 10) { result.add(s); } } return s; }).forEach( e -> {}); System.out.println(result); } }
Si ejecutamos el programa anterior, obtendremos resultados diferentes porque depende de la forma en que se itera el flujo y no tenemos ningún orden definido para el procesamiento paralelo. Si usamos un flujo secuencial, entonces este problema no surgirá.
-
Una vez que un Stream es consumido, no se puede utilizar más tarde. Como puedes ver en los ejemplos anteriores, cada vez que estoy creando un stream.
-
Hay muchos métodos en la API de Stream y la parte más confusa son los métodos sobrecargados. Esto hace que la curva de aprendizaje sea más larga.
Eso es todo para el tutorial de ejemplo de Java 8 Stream. Estoy deseando utilizar esta característica y hacer que el código sea más legible con un mejor rendimiento a través del procesamiento paralelo. Referencia: Documentación de la API de Java Stream
Source:
https://www.digitalocean.com/community/tutorials/java-8-stream