Operaciones de flujo intermedio no evaluadas en conteo

33

Parece que tengo problemas para entender cómo Java compone las operaciones de transmisión en una tubería de transmisión.

Al ejecutar el siguiente código

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

La consola solo imprime 4. El StringBuilderobjeto todavía tiene el valor "".

Cuando agrego la operación de filtro: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

La salida cambia a:

4
1234

¿Cómo cambia esta operación de filtro aparentemente redundante el comportamiento de la tubería de flujo compuesta?

atalantus
fuente
2
Interesante !!!
uneq95
3
Me imagino que este es un comportamiento específico de implementación; tal vez sea porque la primera secuencia tiene un tamaño conocido, pero la segunda no, y el tamaño determina si las operaciones intermedias se ejecutan.
Andy Turner
Por interés, ¿qué sucede si invierte el filtro y el mapa?
Andy Turner
Habiendo programado un poco en Haskell, huele un poco a una evaluación perezosa que está ocurriendo aquí. Una búsqueda en Google regresó, que las transmisiones tienen cierta pereza. ¿Podría ser ese el caso? Y sin un filtro, si Java es lo suficientemente inteligente, no hay necesidad de ejecutar el mapeo.
Frederik
@AndyTurner Da el mismo resultado, incluso en reversión
uneq95

Respuestas:

39

La count()operación del terminal, en mi versión del JDK, termina ejecutando el siguiente código:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

Si hay una filter()operación en la tubería de operaciones, el tamaño de la secuencia, que se conoce inicialmente, ya no se puede conocer (ya que filterpodría rechazar algunos elementos de la secuencia). Por lo tanto, el ifbloque no se ejecuta, las operaciones intermedias se ejecutan y StringBuilder se modifica así.

Por otro lado, si solo tiene map()en la tubería, se garantiza que el número de elementos en la secuencia será el mismo que el número inicial de elementos. Entonces, el bloque if se ejecuta, y el tamaño se devuelve directamente sin evaluar las operaciones intermedias.

Tenga en cuenta que la lambda pasada map()viola el contrato definido en la documentación: se supone que es una operación sin interferencia, sin estado, pero no es sin estado. Por lo tanto, tener un resultado diferente en ambos casos no puede considerarse como un error.

JB Nizet
fuente
Debido a que flatMap()podría cambiar la cantidad de elementos, ¿fue esa la razón por la que inicialmente estaba ansioso (ahora vago)? Entonces, la alternativa sería usar forEach()y contar por separado si map()en su forma actual viola el contrato, supongo.
Frederik
3
En cuanto a flatMap, no lo creo. Era, AFAIK, porque inicialmente era más simple hacerlo ansioso. Sí, usar una secuencia, con map (), para producir efectos secundarios es una mala idea.
JB Nizet
¿Te gustaría una sugerencia sobre cómo lograr la salida completa 4 1234sin utilizar el filtro adicional o producir efectos secundarios en la operación map ()?
atalantus
1
int count = array.length; String result = String.join("", array);
JB Nizet
1
o podría usar forEach si realmente desea usar un StringBuilder, o podría usarCollectors.joining("")
njzk2
19

En jdk-9 estaba claramente documentado en java docs

La elusión de los efectos secundarios también puede ser sorprendente. Con la excepción de las operaciones de terminal para cada uno y para cada pedido, los efectos secundarios de los parámetros de comportamiento pueden no siempre ejecutarse cuando la implementación del flujo puede optimizar la ejecución de los parámetros de comportamiento sin afectar el resultado del cálculo. (Para ver un ejemplo específico, consulte la nota API documentada en la operación de conteo ).

Nota de API:

Una implementación puede optar por no ejecutar el flujo de la secuencia (ya sea secuencialmente o en paralelo) si es capaz de calcular el recuento directamente desde la fuente de la secuencia. En tales casos, no se atravesarán elementos fuente y no se evaluarán operaciones intermedias. Los parámetros de comportamiento con efectos secundarios, que se desaconsejan fuertemente, excepto en casos inofensivos como la depuración, pueden verse afectados. Por ejemplo, considere la siguiente secuencia:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

El número de elementos cubiertos por la fuente del flujo, una Lista, es conocido y la operación intermedia, peek, no inyecta ni elimina elementos del flujo (como puede ser el caso de flatMap o operaciones de filtro). Por lo tanto, el recuento es el tamaño de la Lista y no hay necesidad de ejecutar la canalización y, como efecto secundario, imprimir los elementos de la lista.

Consorcio inactivo
fuente
0

Esto no es para lo que sirve .map. Se supone que debe usarse para convertir una secuencia de "Algo" en una secuencia de "Algo más". En este caso, está utilizando el mapa para agregar una cadena a un Stringbuilder externo, después de lo cual tiene una secuencia de "Stringbuilder", cada uno de los cuales fue creado por la operación de mapa agregando un número al Stringbuilder original.

Su flujo en realidad no hace nada con resultados mapeados en el flujo, por lo que es perfectamente razonable suponer que el procesador de flujo puede omitir el paso. Estás contando con efectos secundarios para hacer el trabajo, lo que rompe el modelo funcional del mapa. Sería mejor servirte usando forEach para hacer esto. Haga el recuento como una secuencia separada por completo, o coloque un contador usando AtomicInt en forEach.

El filtro lo obliga a ejecutar el contenido del flujo ya que ahora tiene que hacer algo nocionalmente significativo con cada elemento del flujo.

DaveB
fuente