¿Por qué es necesario un combinador para reducir el método que convierte el tipo en Java?

Tengo problemas para comprender completamente el rol que cumple el combiner en el método de reduce flujos.

Por ejemplo, el siguiente código no comstack:

 int length = asList("str1", "str2").stream() .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length()); 

El error de comstackción dice: (argumento no coincide; int no se puede convertir a java.lang.String)

pero este código se comstack:

 int length = asList("str1", "str2").stream() .reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(), (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); 

Entiendo que el método combinador se utiliza en transmisiones paralelas, por lo que en mi ejemplo, se sumn dos entradas intermedias acumuladas.

Pero no entiendo por qué el primer ejemplo no se comstack sin el combinador o cómo el combinador está resolviendo la conversión de cadena a int ya que solo está sumndo dos entradas.

¿Alguien puede arrojar luz sobre esto?

Las versiones de dos y tres argumentos de reduce que trataste de usar no aceptan el mismo tipo para el accumulator .

La reduce dos argumentos se define como :

 T reduce(T identity, BinaryOperator accumulator) 

En su caso, T es Cadena, por lo que BinaryOperator debería aceptar dos argumentos de Cadena y devolver una Cadena. Pero le pasas un int y un String, lo que da como resultado el error de comstackción que tienes – argument mismatch; int cannot be converted to java.lang.String argument mismatch; int cannot be converted to java.lang.String . En realidad, creo que pasar 0 como valor de identidad también es incorrecto aquí, ya que se espera una cadena (T).

También tenga en cuenta que esta versión de reduce procesa una secuencia de Ts y devuelve una T, por lo que no puede usarla para reducir una secuencia de String a una int.

La reduce tres argumentos se define como :

  U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) 

En su caso, U es Integer y T es String, por lo que este método reducirá una secuencia de String a Integer.

Para BiFunction BiFunction acumulador BiFunction puede pasar parámetros de dos tipos diferentes (U y? super T), que en su caso son Entero y Cadena. Además, el valor de identidad U acepta un entero en su caso, por lo que pasarlo 0 está bien.

Otra forma de lograr lo que quieres:

 int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) .reduce(0, (accumulatedInt, len) -> accumulatedInt + len); 

Aquí, el tipo de transmisión coincide con el tipo de devolución de reduce , por lo que puede usar la versión de reduce de dos parámetros.

Por supuesto, no tiene que usar reduce en absoluto:

 int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) .sum(); 

La respuesta de Eran describió las diferencias entre las versiones two-arg y three-arg de reduce en que la primera reduce Stream a T mientras que la segunda reduce Stream a U Sin embargo, en realidad no explicaba la necesidad de la función del combinador adicional cuando se reducía el Stream a U

Uno de los principios de diseño de la API de Streams es que la API no debe diferir entre las secuencias secuenciales y paralelas, o dicho de otra manera, una API particular no debe impedir que una secuencia se ejecute correctamente de forma secuencial o en paralelo. Si sus lambdas tienen las propiedades correctas (asociativas, no interferentes, etc.), una secuencia ejecutada secuencialmente o en paralelo debería dar los mismos resultados.

Consideremos primero la versión de dos arg de la reducción:

 T reduce(I, (T, T) -> T) 

La implementación secuencial es directa. El valor de identidad I se “acumula” con el elemento de flujo zeroth para dar un resultado. Este resultado se acumula con el primer elemento de flujo para dar otro resultado, que a su vez se acumula con el segundo elemento de flujo, y así sucesivamente. Después de acumular el último elemento, se devuelve el resultado final.

La implementación paralela comienza dividiendo el flujo en segmentos. Cada segmento se procesa por su propio hilo de la manera secuencial que describí anteriormente. Ahora, si tenemos N hilos, tenemos N resultados intermedios. Estos deben reducirse a un solo resultado. Como cada resultado intermedio es de tipo T y tenemos varios, podemos usar la misma función de acumulador para reducir esos N resultados intermedios a un único resultado.

Consideremos ahora una operación hipotética de reducción de dos arcos que reduce Stream a U En otros idiomas esto se denomina operación de “plegado” o “plegado a la izquierda”, así es como lo llamaré aquí. Tenga en cuenta que esto no existe en Java.

 U foldLeft(I, (U, T) -> U) 

(Tenga en cuenta que el valor de identidad I es del tipo U)

La versión secuencial de foldLeft es como la versión secuencial de reduce excepto que los valores intermedios son del tipo U en lugar del tipo T. Pero es lo mismo. (Una operación hipotética de foldRight sería similar, excepto que las operaciones se realizarían de derecha a izquierda en lugar de izquierda a derecha).

Ahora considere la versión paralela de foldLeft . Comencemos dividiendo la secuencia en segmentos. Entonces podemos hacer que cada uno de los N hilos reduzca los valores T en su segmento en N valores intermedios de tipo U. ¿Y ahora qué? ¿Cómo podemos obtener desde N valores de tipo U hasta un único resultado de tipo U?

Lo que falta es otra función que combina los múltiples resultados intermedios del tipo U en un único resultado de tipo U. Si tenemos una función que combina dos valores U en uno, eso es suficiente para reducir cualquier cantidad de valores a uno, al igual que la reducción original arriba. Por lo tanto, la operación de reducción que da un resultado de un tipo diferente necesita dos funciones:

 U reduce(I, (U, T) -> U, (U, U) -> U) 

O, usando la syntax de Java:

  U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) 

En resumen, para hacer una reducción paralela a un tipo de resultado diferente, necesitamos dos funciones: una que acumula elementos T a valores U intermedios, y una segunda que combina los valores U intermedios en un único resultado U. Si no estamos cambiando de tipos, resulta que la función del acumulador es la misma que la función del combinador. Es por eso que la reducción al mismo tipo solo tiene la función de acumulador y la reducción a un tipo diferente requiere funciones separadas de combinador y combinador.

Finalmente, Java no proporciona operaciones foldLeft y foldRight porque implican un orden particular de operaciones que es intrínsecamente secuencial. Esto choca con el principio de diseño indicado anteriormente de proporcionar API que admiten la operación secuencial y paralela por igual.

Como me gustan los garabatos y las flechas para aclarar conceptos … ¡comencemos!

De cadena a cadena (flujo secuencial)

Supongamos que tiene 4 cadenas: su objective es concatenar tales cadenas en una sola. Básicamente comienzas con un tipo y terminas con el mismo tipo.

Puedes lograr esto con

 String res = Arrays.asList("one", "two","three","four") .stream() .reduce("", (accumulatedStr, str) -> accumulatedStr + str); //accumulator 

y esto te ayuda a visualizar lo que está sucediendo:

enter image description here

La función de acumulador convierte, paso a paso, los elementos en su flujo (rojo) en el valor final reducido (verde). La función de acumulador simplemente transforma un objeto String en otra String .

De cadena a int (secuencia paralela)

Supongamos que tiene las mismas 4 cadenas: su nueva meta es sumr sus longitudes, y desea paralelizar su flujo.

Lo que necesitas es algo como esto:

 int length = Arrays.asList("one", "two","three","four") .parallelStream() .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length(), //accumulator (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner 

y este es un esquema de lo que está sucediendo

enter image description here

Aquí la función de acumulador (una BiFunction ) le permite transformar sus datos de String datos int . Al ser la stream paralela, se divide en dos partes (rojas), cada una de las cuales se elabora de forma independiente entre sí y produce el mismo número de resultados parciales (naranjas). Se necesita definir un combinador para proporcionar una regla para fusionar resultados int parciales en el int (verde) final.

De cadena a int (flujo secuencial)

¿Qué sucede si no quieres paralelizar tu transmisión? Bueno, un combinador necesita ser provisto de todos modos, pero nunca será invocado, dado que no se producirán resultados parciales.

No hay una versión reducida que tome dos tipos diferentes sin un combinador, ya que no se puede ejecutar en paralelo (no estoy seguro de por qué es un requisito). El hecho de que el acumulador debe ser asociativo hace que esta interfaz sea bastante inútil ya que:

 list.stream().reduce(identity, accumulator, combiner); 

Produce los mismos resultados que:

 list.stream().map(i -> accumulator(identity, i)) .reduce(identity, combiner);