Java 8 Streams - coletar vs reduzir

143

Quando você usaria collect()vs reduce()? Alguém tem exemplos bons e concretos de quando é definitivamente melhor seguir um caminho ou outro?

Javadoc menciona que collect () é uma redução mutável .

Dado que é uma redução mutável, presumo que exija sincronização (internamente), o que, por sua vez, pode ser prejudicial ao desempenho. Presumivelmente, reduce()é mais fácil paralelamente ao custo de ter que criar uma nova estrutura de dados para retorno após cada etapa da redução.

As declarações acima são suposições, no entanto, e eu adoraria que um especialista falasse aqui.

jimhooker2002
fonte
1
O restante da página a que você vinculou explica: Assim como em reduza (), um benefício de expressar coletar dessa maneira abstrata é que ele é diretamente passível de paralelização: podemos acumular resultados parciais em paralelo e combiná-los, desde que as funções de acumulação e combinação atendem aos requisitos adequados.
JB Nizet
1
veja também "Streams no Java 8: Reduzir vs. Coletar", de Angelika Langer - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

Respostas:

115

reduceé uma operação " fold ", aplica um operador binário a cada elemento no fluxo, em que o primeiro argumento para o operador é o valor de retorno do aplicativo anterior e o segundo argumento é o elemento de fluxo atual.

collecté uma operação de agregação em que uma "coleção" é criada e cada elemento é "adicionado" a essa coleção. Coleções em diferentes partes do fluxo são adicionadas.

O documento que você vinculou fornece o motivo de ter duas abordagens diferentes:

Se quiséssemos pegar um fluxo de cadeias e concatená-las em uma única cadeia longa, poderíamos conseguir isso com redução comum:

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

Obteríamos o resultado desejado, e isso funcionaria até em paralelo. No entanto, podemos não estar felizes com o desempenho! Essa implementação faria uma grande quantidade de cópias de strings e o tempo de execução seria O (n ^ 2) no número de caracteres. Uma abordagem mais eficiente seria acumular os resultados em um StringBuilder, que é um contêiner mutável para acumular seqüências. Podemos usar a mesma técnica para paralelizar a redução mutável, como fazemos com a redução comum.

Portanto, o ponto é que a paralelização é a mesma nos dois casos, mas, no reducecaso, aplicamos a função aos próprios elementos do fluxo. No collectcaso, aplicamos a função a um contêiner mutável.

Boris, a Aranha
fonte
1
Se este for o caso da coleta: "Uma abordagem mais eficiente seria acumular os resultados em um StringBuilder", por que usaríamos o reduzir?
jimhooker2002
2
@ Jimhooker2002 releu. Por exemplo, se você está calculando o produto, a função de redução pode ser simplesmente aplicada às correntes divididas em paralelo e depois combinadas no final. O processo de redução sempre resulta no tipo como o fluxo. A coleta é usada quando você deseja coletar os resultados em um contêiner mutável, ou seja, quando o resultado é um tipo diferente do fluxo. Isso tem a vantagem de que uma única instância do contêiner pode ser usada para cada fluxo dividido, mas a desvantagem de que os contêineres precisam ser combinados no final.
Boris, a Aranha
1
@ jimhooker2002 no exemplo do produto inté imutável, portanto você não pode usar prontamente uma operação de coleta. Você poderia fazer um truque sujo como usar um AtomicIntegerou algum costume, IntWrappermas por que faria? Uma operação de dobra é simplesmente diferente de uma operação de coleta.
Boris the Spider
17
Há também outro reducemétodo, no qual você pode retornar objetos do tipo diferentes dos elementos do fluxo.
damluar
1
mais um caso em que você usaria coletar em vez de reduzir é quando a operação reduzir envolve adicionar elementos a uma coleção, e toda vez que sua função acumuladora processa um elemento, ela cria uma nova coleção que inclui o elemento, que é ineficiente.
Raghu 12/0518
40

A razão é simplesmente que:

  • collect() só pode trabalhar com objetos de resultado mutáveis .
  • reduce()foi projetado para trabalhar com objetos de resultado imutáveis .

exemplo " reduce()com imutável"

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);
}

exemplo " collect()com mutável"

Por exemplo, se você deseja calcular manualmente uma soma usando-a, collect()ela não pode funcionar, BigDecimalmas apenas com MutableIntfrom, org.apache.commons.lang.mutablepor exemplo. Vejo:

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);
}

Isso funciona porque o acumulador container.add(employee.getSalary().intValue()); não deve retornar um novo objeto com o resultado, mas alterar o estado do mutável containerdo tipo MutableInt.

Se você gostaria de usar BigDecimal, em vez do containerque você não poderia usar o collect()método que container.add(employee.getSalary());não mudaria o containerporque BigDecimalele é imutável. (Além disso BigDecimal::new, não funcionaria, pois BigDecimalnão possui construtor vazio)

Sandro
fonte
2
Observe que você está usando um Integerconstrutor ( new Integer(6)), que foi descontinuado em versões posteriores do Java.
MC Emperor
1
Boa captura @MCEmperor! Eu mudei paraInteger.valueOf(6)
Sandro
@Sandro - Estou confuso. Por que você diz que collect () funciona apenas com objetos mutáveis? Eu usei para concatenar seqüências de caracteres. String allNames = employee.stream () .map (Employee :: getNameString) .collect (Collectors.joining (",")) .toString ();
MasterJoe2 7/03
1
@ MasterJoe2 É simples. Em resumo - a implementação ainda usa o StringBuilderque é mutável. Veja: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
Sandro
30

A redução normal visa combinar dois valores imutáveis , como int, double, etc. e produzir um novo; é uma redução imutável . Por outro lado, o método de coleta é projetado para alterar um contêiner para acumular o resultado que ele deveria produzir.

Para ilustrar o problema, suponhamos que você queira obter Collectors.toList()uma redução simples 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;
        });

Isso é equivalente a Collectors.toList(). No entanto, neste caso, você modifica o List<Integer>. Como sabemos que ArrayListnão é seguro para threads, nem é seguro adicionar / remover valores dele durante a iteração, para que você receba uma exceção simultânea ArrayIndexOutOfBoundsExceptionou qualquer outro tipo de exceção (especialmente quando executado em paralelo) ao atualizar a lista ou o combinador tenta mesclar as listas porque você está mudando a lista acumulando (adicionando) os números inteiros a ela. Se você deseja tornar esse thread seguro, precisará passar uma nova lista sempre que prejudicar o desempenho.

Por outro lado, os Collectors.toList()trabalhos de maneira semelhante. No entanto, garante a segurança do encadeamento quando você acumula os valores na lista. Na documentação do collectmétodo :

Executa uma operação de redução mutável nos elementos desse fluxo usando um Coletor. Se o fluxo for paralelo e o Coletor for simultâneo, ou o fluxo não estiver ordenado ou o coletor não estiver ordenado, será executada uma redução simultânea. Quando executados em paralelo, vários resultados intermediários podem ser instanciados, preenchidos e mesclados para manter o isolamento de estruturas de dados mutáveis. Portanto, mesmo quando executado em paralelo com estruturas de dados não thread-safe (como ArrayList), nenhuma sincronização adicional é necessária para uma redução paralela.

Então, para responder sua pergunta:

Quando você usaria collect()vs reduce()?

se você tiver valores imutáveis, como ints, doubles, Stringsredução que o normal funciona muito bem. No entanto, se você tiver que reducedigitar seus valores a List(estrutura de dados mutáveis), precisará usar a redução mutável com o collectmétodo

George
fonte
No trecho de código, acho que o problema é que ele pega a identidade (nesse caso, uma única instância de um ArrayList) e assume que é "imutável" para que eles possam iniciar xthreads, cada um "adicionando à identidade" e depois combinando. Bom exemplo.
Rogerdpack #
por que receberíamos uma exceção de modificação simultânea, os fluxos de chamada apenas retrocederão o fluxo serial e o que significa que será processado por um único thread e a função combinadora não é chamada?
você precisa saber é o seguinte
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); } }Eu tentei e não obter CCM exceção
Amarnath harish
@amarnathharish O problema ocorre quando você tenta executá-lo em paralelo e vários segmentos tentarem acessar a mesma lista
george
11

Seja o fluxo a <- b <- c <- d

Em redução,

você terá ((a # b) # c) # d

onde # é aquela operação interessante que você gostaria de fazer.

Na coleção,

seu colecionador terá algum tipo de estrutura de coleta K.

K consome a. K então consome b. K então consome c. K então consome d.

No final, você pergunta a K qual é o resultado final.

K então dá a você.

Yan Ng
fonte
2

Eles são muito diferentes no espaço potencial de memória durante o tempo de execução. Enquanto collect()coleta e coloca todos os dados na coleção, reduce()solicita explicitamente que você especifique como reduzir os dados que passaram pelo fluxo.

Por exemplo, se você quiser ler alguns dados de um arquivo, processá-lo e colocá-lo em algum banco de dados, poderá acabar com um código de fluxo java semelhante a este:

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

Nesse caso, usamos collect()para forçar o java a transmitir dados e salvar o resultado no banco de dados. Sem collect()os dados nunca é lido e nunca é armazenado.

Felizmente, esse código gera um java.lang.OutOfMemoryError: Java heap spaceerro de tempo de execução, se o tamanho do arquivo for grande o suficiente ou o tamanho da pilha for baixo o suficiente. O motivo óbvio é que ele tenta empilhar todos os dados que passaram pelo fluxo (e, de fato, já foram armazenados no banco de dados) na coleção resultante e isso acaba com a pilha.

No entanto, se você substituir collect()por reduce()- não será mais um problema, pois o último reduzirá e descartará todos os dados que o fizeram.

No exemplo apresentado, basta substituir collect()por algo com reduce:

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

Você não precisa nem se preocupar em fazer o cálculo depender, resultpois o Java não é uma linguagem pura de FP (programação funcional) e não pode otimizar os dados que não estão sendo usados ​​na parte inferior do fluxo devido aos possíveis efeitos colaterais .

averasko
fonte
3
Se você não se importa com os resultados de seu salvamento de banco de dados, deve usar forEach ... você não precisa usar o reduzir. A menos que isso seja para fins ilustrativos.
precisa saber é o seguinte
2

Aqui está o exemplo 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 (sum);

Aqui está o resultado da execução:

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

A função Reduce manipula dois parâmetros, o primeiro parâmetro é o valor de retorno anterior no fluxo, o segundo parâmetro é o valor de cálculo atual no fluxo, soma o primeiro valor e o valor atual como o primeiro valor na próxima caculação.

JetQin
fonte
0

De acordo com os documentos

Os coletores de redução () são mais úteis quando usados ​​em uma redução em vários níveis, a jusante de agrupamentoBy ou particionamentoBy. Para executar uma redução simples em um fluxo, use Stream.reduce (BinaryOperator).

Então, basicamente, você usaria reducing()apenas quando forçado dentro de uma coleção. Aqui está outro exemplo :

 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 acordo com este tutorial, reduzir às vezes é menos eficiente

A operação de redução sempre retorna um novo valor. No entanto, a função acumulador também retorna um novo valor toda vez que processa um elemento de um fluxo. Suponha que você queira reduzir os elementos de um fluxo para um objeto mais complexo, como uma coleção. Isso pode prejudicar o desempenho do seu aplicativo. Se sua operação de redução envolver a adição de elementos a uma coleção, toda vez que sua função acumuladora processar um elemento, ela criará uma nova coleção que inclui o elemento, que é ineficiente. Seria mais eficiente para você atualizar uma coleção existente. Você pode fazer isso com o método Stream.collect, que a próxima seção descreve ...

Portanto, a identidade é "reutilizada" em um cenário de redução, sendo um pouco mais eficiente .reducese possível.

rogerdpack
fonte