streams - ¿Por qué tengo que encadenar las operaciones de Stream en Java?
predicados java 8 (3)
Las operaciones intermedias devuelven un nuevo flujo. Siempre son perezosos; la ejecución de una operación intermedia como filter () no realiza ningún filtrado, sino que crea un nuevo flujo que, cuando se recorre, contiene los elementos del flujo inicial que coinciden con el predicado dado.
Tomado de https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html en "Operaciones y tuberías de la corriente"
En el nivel más bajo, todas las corrientes son impulsadas por un separador.
Tomado del mismo enlace en "Construcción de arroyos de bajo nivel"
Elementos de escape transversales y divisorios; Cada Spliterator es útil para un solo cálculo masivo.
Tomado de https://docs.oracle.com/javase/8/docs/api/java/util/Spliterator.html
Esta pregunta ya tiene una respuesta aquí:
Creo que todos los recursos que he estudiado de una u otra manera enfatizan que un flujo se puede consumir solo una vez, y el consumo se realiza mediante las llamadas operaciones de terminal (lo cual es muy claro para mí).
Solo por curiosidad probé esto:
import java.util.stream.IntStream;
class App {
public static void main(String[] args) {
IntStream is = IntStream.of(1, 2, 3, 4);
is.map(i -> i + 1);
int sum = is.sum();
}
}
que termina lanzando una excepción de tiempo de ejecución:
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.IntPipeline.reduce(IntPipeline.java:456)
at java.util.stream.IntPipeline.sum(IntPipeline.java:414)
at App.main(scratch.java:10)
Esto es habitual, me falta algo, pero todavía quiero preguntar: Hasta donde sé, el map
es una operación intermedia (y perezosa) y no hace nada en el Stream por sí solo. Solo cuando se llama a la sum
operación del terminal (que es una operación ávida), la corriente se consume y se opera .
Pero ¿por qué tengo que encadenarlos?
Cuál es la diferencia entre
is.map(i -> i + 1);
is.sum();
y
is.map(i -> i + 1).sum();
?
Cuando haces esto:
int sum = IntStream.of(1, 2, 3, 4).map(i -> i + 1).sum();
Cada método encadenado se invoca en el valor de retorno del método anterior en la cadena.
Así que el map
se invoca en lo que IntStream.of(1, 2, 3, 4)
devuelve y sum
en qué map(i -> i + 1)
devuelve.
No tiene que encadenar métodos de transmisión, pero es más legible y menos propenso a errores que usar este código equivalente:
IntStream is = IntStream.of(1, 2, 3, 4);
is = is.map(i -> i + 1);
int sum = is.sum();
Que no es lo mismo que el código que has mostrado en tu pregunta:
IntStream is = IntStream.of(1, 2, 3, 4);
is.map(i -> i + 1);
int sum = is.sum();
Como ve, no hace caso de la referencia devuelta por el map
. Esta es la causa del error.
EDITAR (según los comentarios, gracias a @IanKemp por señalarlo): En realidad, esta es la causa externa del error. Si se detiene a pensar en ello, el map
debe estar haciendo algo internamente al flujo mismo; de lo contrario, ¿cómo la operación del terminal desencadenará la transformación que se pasa al map
en cada elemento? Estoy de acuerdo en que las operaciones intermedias son perezosas, es decir, cuando se invocan, no hacen nada a los elementos del flujo. Pero internamente, deben configurar algún estado en el flujo de la línea misma, para que puedan aplicarse más tarde.
A pesar de que no estoy al tanto de todos los detalles, lo que sucede es que, conceptualmente, el map
está haciendo al menos 2 cosas:
Está creando y devolviendo un nuevo flujo que mantiene la función pasada como un argumento en algún lugar, de modo que se pueda aplicar a los elementos más adelante, cuando se invoca la operación del terminal.
También está configurando un indicador para la instancia de secuencia antigua, es decir, la que se ha activado, lo que indica que esta instancia de secuencia ya no representa un estado válido para la canalización. Esto se debe a que la instancia que ha devuelto ahora encapsula el nuevo estado actualizado que contiene la función que se pasó al
map
. (Creo que el equipo jdk podría haber tomado esta decisión para hacer que los errores aparezcan lo antes posible, es decir, al lanzar una excepción temprana en lugar de dejar que la tubería continúe con un estado no válido / antiguo que no mantiene la función a aplicar, permitiendo así que la operación del terminal devuelva resultados inesperados).
Más adelante, cuando se invoca una operación de terminal en esta instancia marcada como no válida, obtendrá esa IllegalStateException
. Los dos elementos anteriores configuran la causa interna profunda del error.
Otra forma de ver todo esto es asegurarse de que una instancia de Stream
se opere solo una vez, por medio de una operación intermedia o de terminal. Aquí estás violando este requisito, porque estás llamando map
y sum
en la misma instancia.
De hecho, los javadocs para Stream
indican claramente:
Un flujo debe operarse en (invocar una operación de flujo intermedio o terminal) solo una vez. Esto descarta, por ejemplo, los flujos "bifurcados", donde la misma fuente alimenta dos o más conductos, o múltiples recorridos del mismo flujo. Una implementación de flujo puede lanzar la
IllegalStateException
si detecta que el flujo se está reutilizando. Sin embargo, dado que algunas operaciones de transmisión pueden devolver su receptor en lugar de un nuevo objeto de transmisión, puede que no sea posible detectar la reutilización en todos los casos.
Imagine que IntStream es una envoltura alrededor de su flujo de datos con una lista inmutable de operaciones. Estas operaciones no se ejecutan hasta que necesite el resultado final (suma en su caso). Como la lista es inmutable, necesita una nueva instancia de IntStream con una lista que contenga los elementos anteriores más el nuevo, que es lo que es ''. Mapa ''vuelve.
Esto significa que si no encadena, operará en la instancia anterior, que no tiene esa operación.
La biblioteca de secuencias también mantiene un seguimiento interno de lo que está sucediendo, por eso es capaz de lanzar la excepción en el paso de la sum
.
Si no quiere encadenar, puede usar una variable para cada paso:
IntStream is = IntStream.of(1, 2, 3, 4);
IntStream is2 = is.map(i -> i + 1);
int sum = is2.sum();