Los streams son una de las principales funciones nuevas de Java 8.
En este tutorial, discutiremos un tema interesante: la diferencia entre Stream.of() e IntStream.range().
El Problema
Podemos inicializar un objeto Stream usando el método Stream.of(), por ejemplo, Stream.of(1, 2, 3, 4, 5). Alternativamente, si queremos inicializar un Stream de enteros, IntStream es un tipo más sencillo de usar, por ejemplo, IntStream.range(1, 6). Sin embargo, los comportamientos de los flujos enteros creados por estos dos enfoques pueden ser diferentes.
Como de costumbre, entenderemos el problema a través de un ejemplo. Primero, creemos dos Streams de diferentes maneras:
Stream<Integer> streamNormal = Stream.of(1, 2, 3, 4, 5);
IntStream intStream = IntStream.range(1, 6);
A continuación, ejecutaríamos la misma rutina en los dos Streams anteriores:
STREAM.peek(add to a result list)
.sorted()
.findFirst();
Entonces, invocamos tres métodos en cada Stream:
- primero: invoque el método peek() para recopilar elementos procesados en una lista de resultados
- luego – ordenar los elementos
- finalmente – toma el primer elemento del Strea
Como los dos Streams contienen los mismos elementos enteros, pensaríamos que después de las ejecuciones, las dos listas de resultados también deberían contener los mismos enteros. Entonces, a continuación, escribamos una prueba para verificar si produce el resultado que esperamos:
List<Integer> streamNormalResultadosPeek = new ArrayList<>();
List<Integer> intStreamResultadosPeek = new ArrayList<>();
// Primero, el stream normal
streamNormal.peek(streamNormalResultadosPeek::add)
.sorted()
.findFirst();
assertEquals(Arrays.asList(1, 2, 3, 4, 5), streamNormalResultadosPeek);
// Ahora con el intStream
intStream.peek(intStreamResultadosPeek::add)
.sorted()
.findFirst();
assertEquals(Arrays.asList(1), intStreamResultadosPeek);
Después de la ejecución, resulta que la lista de resultados rellenada por streamNormal.peek() contiene todos los elementos enteros. Sin embargo, la lista completada por intStream.peek() tiene solo un elemento.
A continuación, averigüemos por qué funciona así.
Lazy Streams
Antes de explicar por qué los dos streams produjeron diferentes listas de resultados en la prueba anterior, entendamos que los streams de Java son “lazy” por diseño.
El “lazy” significa que los flujos solo realizan las operaciones deseadas cuando se les indica que produzcan un resultado. En otras palabras, las operaciones intermedias en un Stream no se ejecutan hasta que se realiza una operación de terminal. Este comportamiento perezoso puede ser una ventaja, ya que permite un procesamiento más eficiente y evita cálculos innecesarios.
Para comprender rápidamente este comportamiento perezoso, eliminemos temporalmente la llamada al método sort() de nuestra prueba anterior y volvamos a ejecutarla:
List<Integer> streamNormalPeek = new ArrayList<>();
List<Integer> intStreamPeek = new ArrayList<>();
// Primero el stream normal
streamNormal.peek(streamNormalPeek::add)
.findFirst();
assertEquals(Arrays.asList(1), streamNormalPeek);
// Ahora el intStream
intStream.peek(intStreamPeek::add)
.findFirst();
assertEquals(Arrays.asList(1), intStreamPeek);
Ambos Streams han llenado solo el primer elemento en la lista de resultados correspondiente esta vez. Esto se debe a que el método findFirst() es la operación de terminal y solo requiere un elemento: el primero.
Ahora que entendemos que los Streams son perezosos, a continuación, averigüemos por qué las dos listas de resultados son diferentes cuando el método sorted() se une a la fiesta.
El método sorted() convierte el stream en “eager”
Primero, echemos un vistazo al Stream inicializado por Stream.of(). La operación de terminal findFirst() solo requiere el primer entero en Stream. Pero es el primero después de la operación sorted().
Sabemos que debemos recorrer todos los enteros para ordenarlos. Por lo tanto, llamar a sorted() ha convertido el Stream en “eager” (ansioso). Entonces, el método peek() se llama en cada elemento.
Por otro lado, IntStream.range() devuelve un IntStream ordenado secuencialmente. Es decir, la entrada del objeto IntStream ya está ordenada. Además, cuando ordena una entrada ya ordenada, Java aplica la optimización para hacer que la operación sorted() no funcione. Por lo tanto, todavía tenemos un solo elemento en la lista de resultados.
A continuación, veamos otro ejemplo de un Stream basado en TreeSet:
List<String> peekResult = new ArrayList<>();
TreeSet<String> treeSet = new TreeSet<>(Arrays.asList("CCC", "BBB", "AAA", "DDD", "KKK"));
treeSet.stream()
.peek(peekResult::add)
.sorted()
.findFirst();
assertEquals(Arrays.asList("AAA"), peekResult);
Sabemos que TreeSet es una colección ordenada. Por lo tanto, vemos que la lista peekResult contiene solo una cadena, aunque la hemos llamado sorted().
Conclusión
En este artículo, tomamos Stream.of() e IntStream.range() como ejemplos para comprender que llamar a sorted() puede convertir un Stream de “lazy” a “eager”.