Tengo problemas para comprender completamente el papel que combiner
cumple el reduce
método Streams .
Por ejemplo, el siguiente código no se compila:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());
El error de compilación dice: (discrepancia de argumento; int no se puede convertir a java.lang.String)
pero este código compila:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(),
(accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);
Entiendo que el método combinador se usa en corrientes paralelas, por lo que en mi ejemplo se suman dos entradas intermedias acumuladas.
Pero no entiendo por qué el primer ejemplo no se compila sin el combinador o cómo el combinador está resolviendo la conversión de string a int ya que solo está sumando dos ints.
¿Alguien puede arrojar luz sobre esto?
java
java-8
java-stream
Louise Miller
fuente
fuente
Respuestas:
Las versiones de dos y tres argumentos
reduce
que intentó utilizar no aceptan el mismo tipo paraaccumulator
.Los dos argumentos
reduce
se definen como :En su caso, T es una cadena, por lo que
BinaryOperator<T>
debe aceptar dos argumentos de cadena y devolver una cadena. Pero le pasa un int y una cadena, lo que da como resultado el error de compilación que obtuvo -argument mismatch; int cannot be converted to java.lang.String
. En realidad, creo que pasar 0 ya que el valor de identidad también está mal 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 un int.
Los tres argumentos
reduce
se definen como :En su caso, U es Entero y T es Cadena, por lo que este método reducirá una secuencia de Cadena a un Entero.
Para el
BiFunction<U,? super T,U>
acumulador puede pasar parámetros de dos tipos diferentes (U y? Super T), que en su caso son Integer y String. Además, el valor de identidad U acepta un número entero en su caso, por lo que pasarlo 0 está bien.Otra forma de lograr lo que quieres:
Aquí el tipo de la secuencia coincide con el tipo de retorno de
reduce
, por lo que puede utilizar la versión de dos parámetros dereduce
.Por supuesto, no tienes que usar
reduce
nada:fuente
mapToInt(String::length)
overmapToInt(s -> s.length())
, no estoy seguro de si uno sería mejor sobre el otro, pero prefiero el primero para facilitar la lectura.combiner
es necesaria, por qué no tenerlaaccumulator
es suficiente. En ese caso: el combinador solo es necesario para corrientes paralelas, para combinar los resultados "acumulados" de los hilos.La respuesta de Eran describió las diferencias entre las versiones de dos argumentos y tres argumentos de
reduce
que el primero se reduceStream<T>
aT
mientras que el segundo se reduceStream<T>
aU
. Sin embargo, en realidad no explicaba la necesidad de la función de combinador adicional cuando se reduceStream<T>
aU
.Uno de los principios de diseño de la API de Streams es que la API no debe diferir entre secuencias secuenciales y paralelas, o dicho de otra manera, una API en particular no debe evitar 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 reducción de dos argumentos:
La implementación secuencial es sencilla. El valor de identidad
I
se "acumula" con el elemento de flujo cero 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 que se acumula el último elemento, se devuelve el resultado final.La implementación paralela comienza dividiendo la secuencia en segmentos. Cada segmento es procesado por su propio hilo en la forma 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 solo resultado.
Ahora consideremos una operación hipotética de reducción de dos argumentos que se reduce
Stream<T>
aU
. En otros idiomas, esto se llama operación "doblar" o "doblar a la izquierda", así que así lo llamaré aquí. Tenga en cuenta que esto no existe en Java.(Tenga en cuenta que el valor de identidad
I
es del tipo U).La versión secuencial de
foldLeft
es igual que la versión secuencial de,reduce
excepto que los valores intermedios son del tipo U en lugar del tipo T. Pero, por lo demás, es el mismo. (UnafoldRight
operación hipotética 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 de T en su segmento en N valores intermedios de tipo U. ¿Y ahora qué? ¿Cómo pasamos de N valores de tipo U a un solo resultado de tipo U?Lo que falta es otra función que combine los múltiples resultados intermedios del tipo U en un solo resultado del tipo U. Si tenemos una función que combina dos valores U en uno, es suficiente para reducir cualquier número de valores a uno, al igual que La reducción original anterior. Por lo tanto, la operación de reducción que da un resultado de un tipo diferente necesita dos funciones:
O, usando la sintaxis de Java:
En resumen, para hacer una reducción paralela a un tipo de resultado diferente, necesitamos dos funciones: una que acumule elementos T a valores U intermedios, y una segunda que combine los valores U intermedios en un único resultado U. Si no estamos cambiando 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 de acumulador y combinador separadas.
Finalmente, Java no proporciona operaciones
foldLeft
yfoldRight
operaciones porque implican un orden particular de operaciones que es inherentemente secuencial. Esto choca con el principio de diseño establecido anteriormente de proporcionar API que admitan la operación secuencial y paralela por igual.fuente
foldLeft
porque el cálculo depende del resultado anterior y no se puede paralelizar?forEachOrdered
. Sin embargo, el estado intermedio debe mantenerse en una variable capturada.foldLeft
.Como me gustan los garabatos y las flechas para aclarar conceptos ... ¡comencemos!
De cadena a cadena (secuencia secuencial)
Supongamos que tiene 4 cadenas: su objetivo es concatenar tales cadenas en una sola. Básicamente comienzas con un tipo y terminas con el mismo tipo.
Puedes lograr esto con
y esto te ayuda a visualizar lo que está sucediendo:
La función de acumulador convierte, paso a paso, los elementos en su flujo (rojo) al valor final reducido (verde). La función de acumulador simplemente transforma un
String
objeto en otroString
.De cadena a int (flujo paralelo)
Suponga que tiene las mismas 4 cadenas: su nuevo objetivo es sumar sus longitudes y desea paralelizar su flujo.
Lo que necesitas es algo como esto:
y este es un esquema de lo que está sucediendo
Aquí la función de acumulador (a
BiFunction
) le permite transformar susString
datos enint
datos. Al ser la corriente paralela, se divide en dos partes (rojas), cada una de las cuales se elabora independientemente una de la otra y produce la misma cantidad de resultados parciales (naranja). Se necesita definir un combinador para proporcionar una regla para fusionarint
resultados parciales en el final (verde)int
.De cadena a int (secuencia secuencial)
¿Qué pasa si no quieres paralelizar tu transmisión? Bueno, de todos modos, se debe proporcionar un combinador, pero nunca se invocará, dado que no se producirán resultados parciales.
fuente
No existe reducir la versión que tiene dos tipos diferentes sin un combinador ya que no se puede ejecutar en paralelo (no estoy seguro de por qué esto es un requisito). El hecho de que el acumulador debe ser asociativo hace que esta interfaz sea bastante inútil ya que:
Produce los mismos resultados que:
fuente
map
truco depende de lo particularaccumulator
ycombiner
puede ralentizar las cosas más o menos.accumulator
soltando el primer parámetro.