Java 8 Streams: recopilar vs reducir

143

¿Cuándo usarías collect()vs reduce()? ¿Alguien tiene buenos ejemplos concretos de cuándo definitivamente es mejor ir de una manera u otra?

Javadoc menciona que collect () es una reducción mutable .

Dado que es una reducción mutable, supongo que requiere sincronización (internamente) que, a su vez, puede ser perjudicial para el rendimiento. Presumiblemente, reduce()es más fácilmente paralelizable a costa de tener que crear una nueva estructura de datos para el retorno después de cada paso en la reducción.

Sin embargo, las declaraciones anteriores son conjeturas y me encantaría que un experto interviniera aquí.

jimhooker2002
fuente
1
El resto de la página a la que enlazó lo explica: al igual que con reduce (), un beneficio de expresar recoger de esta manera abstracta es que es directamente susceptible de paralelización: podemos acumular resultados parciales en paralelo y luego combinarlos, siempre que Las funciones de acumulación y combinación satisfacen los requisitos apropiados.
JB Nizet
1
también vea "Streams in Java 8: Reduce vs. Collect" por Angelika Langer - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

Respuestas:

115

reducees una operación de " pliegue ", aplica un operador binario a cada elemento en la secuencia donde el primer argumento para el operador es el valor de retorno de la aplicación anterior y el segundo argumento es el elemento actual de la secuencia.

collectes una operación de agregación donde se crea una "colección" y cada elemento se "agrega" a esa colección. Las colecciones en diferentes partes de la secuencia se agregan juntas.

El documento que vinculó da la razón de tener dos enfoques diferentes:

Si quisiéramos tomar una secuencia de cadenas y concatenarlas en una sola cadena larga, podríamos lograr esto con una reducción ordinaria:

 String concatenated = strings.reduce("", String::concat)  

Obtendríamos el resultado deseado, e incluso funcionaría en paralelo. Sin embargo, ¡podríamos no estar contentos con el rendimiento! Tal implementación haría una gran cantidad de copias de cadenas, y el tiempo de ejecución sería O (n ^ 2) en el número de caracteres. Un enfoque más eficaz sería acumular los resultados en un StringBuilder, que es un contenedor mutable para acumular cadenas. Podemos usar la misma técnica para paralelizar la reducción mutable como lo hacemos con la reducción ordinaria.

Entonces, el punto es que la paralelización es la misma en ambos casos, pero en el reducecaso de que apliquemos la función a los elementos de flujo en sí. En el collectcaso, aplicamos la función a un contenedor mutable.

Boris la araña
fuente
1
Si este es el caso de collect: "Un enfoque más eficaz sería acumular los resultados en un StringBuilder", ¿por qué alguna vez usaríamos reduce?
jimhooker2002
2
@ Jimhooker2002 lo releyó. Si, por ejemplo, está calculando el producto, entonces la función de reducción puede simplemente aplicarse a las corrientes divididas en paralelo y luego combinarse al final. El proceso de reducción siempre da como resultado el tipo como la secuencia. La recopilación se utiliza cuando desea recopilar los resultados en un contenedor mutable, es decir, cuando el resultado es de un tipo diferente a la secuencia. Esto tiene la ventaja de que se puede usar una sola instancia del contenedor para cada flujo dividido, pero la desventaja de que los contenedores deben combinarse al final.
Boris the Spider
1
@ jimhooker2002 en el ejemplo del producto, intes inmutable, por lo que no puede utilizar fácilmente una operación de recopilación. Podrías hacer un truco sucio como usar una AtomicIntegero algunas personalizadas, IntWrapperpero ¿por qué lo harías? Una operación de plegado es simplemente diferente a una operación de recolección.
Boris the Spider
17
También hay otro reducemétodo, donde puede devolver objetos de tipo diferente de los elementos de la secuencia.
Damluar
1
un caso más en el que usaría collect en lugar de reduce es cuando la operación reduce implica agregar elementos a una colección, luego, cada vez que su función de acumulador procesa un elemento, crea una nueva colección que incluye el elemento, que es ineficiente.
raghu
40

La razón es simplemente eso:

  • collect() solo puede funcionar con objetos de resultado mutables .
  • reduce()está diseñado para trabajar con objetos de resultados inmutables .

reduce()ejemplo " con inmutable"

public class Employee {
  private Integer salary;
  public Employee(String aSalary){
    this.salary = new Integer(aSalary);
  }
  public Integer getSalary(){
    return this.salary;
  }
}

@Test
public void testReduceWithImmutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));
  list.add(new Employee("3"));

  Integer sum = list
  .stream()
  .map(Employee::getSalary)
  .reduce(0, (Integer a, Integer b) -> Integer.sum(a, b));

  assertEquals(Integer.valueOf(6), sum);
}

collect()ejemplo " con mutable"

Por ejemplo, si desea calcular manualmente una suma con collect()ella, no puede funcionar, BigDecimalsino solo con MutableIntdesde, org.apache.commons.lang.mutablepor ejemplo. Ver:

public class Employee {
  private MutableInt salary;
  public Employee(String aSalary){
    this.salary = new MutableInt(aSalary);
  }
  public MutableInt getSalary(){
    return this.salary;
  }
}

@Test
public void testCollectWithMutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));

  MutableInt sum = list.stream().collect(
    MutableInt::new, 
    (MutableInt container, Employee employee) -> 
      container.add(employee.getSalary().intValue())
    , 
    MutableInt::add);
  assertEquals(new MutableInt(3), sum);
}

Esto funciona porque no se supone que el acumulador container.add(employee.getSalary().intValue()); devuelva un nuevo objeto con el resultado, sino que cambie el estado containerdel tipo mutable MutableInt.

Si desea utilizarlo BigDecimalen su lugar, containerno podría utilizar el collect()método, ya container.add(employee.getSalary());que no cambiaría el containerporque BigDecimales inmutable. (Aparte de esto BigDecimal::newno funcionaría ya que BigDecimalno tiene un constructor vacío)

Sandro
fuente
2
Tenga en cuenta que está utilizando un Integerconstructor ( new Integer(6)), que está en desuso en versiones posteriores de Java.
MC Emperor
1
Buena captura @MCEmperor! Lo he cambiado aInteger.valueOf(6)
Sandro
@Sandro - Estoy confundido. ¿Por qué dices que collect () funciona solo con objetos mutables? Lo usé para concatenar cadenas. Cadena allNames = employee.stream () .map (Employee :: getNameString) .collect (Collectors.joining (",")) .toString ();
MasterJoe2
1
@ MasterJoe2 Es simple. En resumen, la implementación todavía usa el StringBuildercual es mutable. Ver: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
Sandro
30

La reducción normal está destinada a combinar dos valores inmutables como int, double, etc. y producir uno nuevo; Es una reducción inmutable . En contraste, el método de recolección está diseñado para mutar un contenedor para acumular el resultado que se supone que debe producir.

Para ilustrar el problema, supongamos que desea lograrlo Collectors.toList()usando una reducción simple como

List<Integer> numbers = stream.reduce(
        new ArrayList<Integer>(),
        (List<Integer> l, Integer e) -> {
            l.add(e);
            return l;
        },
        (List<Integer> l1, List<Integer> l2) -> {
            l1.addAll(l2);
            return l1;
        });

Este es el equivalente de Collectors.toList(). Sin embargo, en este caso mutas el List<Integer>. Como sabemos ArrayList, no es seguro para subprocesos, ni es seguro agregar / eliminar valores mientras itera, por lo que obtendrá una excepción concurrente ArrayIndexOutOfBoundsExceptiono cualquier tipo de excepción (especialmente cuando se ejecuta en paralelo) cuando actualiza la lista o el combinador intenta fusionar las listas porque está mutando la lista acumulando (agregando) los enteros. Si desea hacer que este hilo sea seguro, debe pasar una nueva lista cada vez que perjudicaría el rendimiento.

En contraste, las Collectors.toList()obras de manera similar. Sin embargo, garantiza la seguridad del hilo cuando acumula los valores en la lista. De la documentación para el collectmétodo :

Realiza una operación de reducción mutable en los elementos de esta secuencia utilizando un recopilador. Si la secuencia es paralela, y el recopilador es concurrente, y la secuencia no está ordenada o el recopilador no está ordenado, se realizará una reducción concurrente. Cuando se ejecuta en paralelo, se pueden instanciar, completar y fusionar múltiples resultados intermedios para mantener el aislamiento de las estructuras de datos mutables. Por lo tanto, incluso cuando se ejecuta en paralelo con estructuras de datos no seguras para subprocesos (como ArrayList), no se necesita sincronización adicional para una reducción paralela.

Entonces para responder a su pregunta:

¿Cuándo usarías collect()vs reduce()?

si tiene valores inmutables, tales como ints, doubles, Stringsa continuación, la reducción de lo normal funciona bien. Sin embargo, si tiene que decir reducesus valores para decir una List(estructura de datos mutable), entonces necesita usar la reducción mutable con el collectmétodo.

Jorge
fuente
En el fragmento de código, creo que el problema es que tomará la identidad (en este caso, una sola instancia de una ArrayList) y supondrá que es "inmutable" para que puedan iniciar xhilos, cada uno "añadiendo a la identidad" y luego combinándose. Buen ejemplo.
rogerdpack
¿Por qué obtendríamos una excepción de modificación concurrente, llamar a las secuencias solo va a volver a ejecutar la secuencia en serie y lo que significa que será procesada por un solo hilo y la función del combinador no se llama en absoluto?
amarnath harish
public static void main(String[] args) { List<Integer> l = new ArrayList<>(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List<Integer> numbers = l.stream().reduce( new ArrayList<Integer>(), (List<Integer> l2, Integer e) -> { l2.add(e); return l2; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } }intenté y no obtuve la excepción
CCm
@amarnathharish el problema ocurre cuando intenta ejecutarlo en paralelo y varios hilos intentan acceder a la misma lista
george
11

Deje que la corriente sea a <- b <- c <- d

En reducción,

usted tendrá ((a # b) # c) # d

donde # es esa operación interesante que te gustaría hacer.

En colección,

su recolector tendrá algún tipo de estructura de recolección K.

K consume a. K entonces consume b. K entonces consume c. K entonces consume d.

Al final, le preguntas a K cuál es el resultado final.

K entonces te lo da.

Yan Ng
fuente
2

Son muy diferentes en la huella de memoria potencial durante el tiempo de ejecución. Mientras collect()recopila y coloca todos los datos en la colección, reduce()le pide explícitamente que especifique cómo reducir los datos que llegaron a través de la transmisión.

Por ejemplo, si desea leer algunos datos de un archivo, procesarlos y colocarlos en alguna base de datos, puede terminar con un código de flujo de Java similar a este:

streamDataFromFile(file)
            .map(data -> processData(data))
            .map(result -> database.save(result))
            .collect(Collectors.toList());

En este caso, usamos collect()para forzar a Java a transmitir datos y hacer que guarde el resultado en la base de datos. Sin collect()los datos nunca se lee y nunca se almacena.

Este código genera felizmente un java.lang.OutOfMemoryError: Java heap spaceerror de tiempo de ejecución, si el tamaño del archivo es lo suficientemente grande o el tamaño del montón es lo suficientemente bajo. La razón obvia es que intenta apilar todos los datos que llegaron a través de la secuencia (y, de hecho, ya se han almacenado en la base de datos) en la colección resultante y esto explota el montón.

Sin embargo, si reemplaza collect()con reduce(), ya no será un problema, ya que este último reducirá y descartará todos los datos que lo lograron.

En el ejemplo presentado, simplemente reemplace collect()con algo con reduce:

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

Ni siquiera necesita preocuparse de hacer que el cálculo dependa de, resultya que Java no es un lenguaje de FP (programación funcional) puro y no puede optimizar los datos que no se utilizan en la parte inferior de la secuencia debido a los posibles efectos secundarios .

averasko
fuente
3
Si no le importan los resultados de su guardado de db, debe usar forEach ... no necesita usar reduce. A menos que esto fuera con fines ilustrativos.
DaveEdelstein
2

Aquí está el ejemplo de código

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().reduce((x,y) -> {
        System.out.println(String.format("x=%d,y=%d",x,y));
        return (x + y);
    }).get();

System.out.println (suma);

Aquí está el resultado de ejecución:

x=1,y=2
x=3,y=3
x=6,y=4
x=10,y=5
x=15,y=6
x=21,y=7
28

La función de reducción maneja dos parámetros, el primer parámetro es el valor de retorno anterior en el flujo, el segundo parámetro es el valor de cálculo actual en el flujo, suma el primer valor y el valor actual como el primer valor en el próximo cálculo.

JetQin
fuente
0

De acuerdo con los documentos

Los colectores reductores () son más útiles cuando se usan en una reducción multinivel, aguas abajo de groupingBy o divisioningBy. Para realizar una reducción simple en una secuencia, use Stream.reduce (BinaryOperator) en su lugar.

Básicamente, reducing()solo lo usarías cuando te veas forzado a recoger. Aquí hay otro ejemplo :

 For example, given a stream of Person, to calculate the longest last name 
 of residents in each city:

    Comparator<String> byLength = Comparator.comparing(String::length);
    Map<String, String> longestLastNameByCity
        = personList.stream().collect(groupingBy(Person::getCity,
            reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));

De acuerdo con este tutorial, reducir es a veces menos eficiente

La operación de reducción siempre devuelve un nuevo valor. Sin embargo, la función de acumulador también devuelve un nuevo valor cada vez que procesa un elemento de una secuencia. Suponga que desea reducir los elementos de una secuencia a un objeto más complejo, como una colección. Esto podría dificultar el rendimiento de su aplicación. Si su operación de reducción implica agregar elementos a una colección, cada vez que su función de acumulador procesa un elemento, crea una nueva colección que incluye el elemento, que es ineficiente. En su lugar, sería más eficiente actualizar una colección existente. Puede hacerlo con el método Stream.collect, que se describe en la siguiente sección ...

Por lo tanto, la identidad se "reutiliza" en un escenario reducido, por lo que es un poco más eficiente .reducesi es posible.

rogerdpack
fuente