Strumienie Java 8 - zbieraj a zmniejszaj

143

Kiedy użyjesz collect()vs reduce()? Czy ktoś ma dobre, konkretne przykłady, kiedy zdecydowanie lepiej jest iść w jedną lub drugą stronę?

Javadoc wspomina, że ​​metoda collect () jest modyfikowalną redukcją .

Biorąc pod uwagę, że jest to redukcja zmienna, zakładam, że wymaga synchronizacji (wewnętrznej), co z kolei może mieć negatywny wpływ na wydajność. Przypuszczalnie reduce()jest łatwiejszy do zrównoleglenia kosztem konieczności tworzenia nowej struktury danych do zwrotu po każdym kroku redukcji.

Powyższe stwierdzenia są jednak domysłami i chciałbym, aby ekspert zabrał tutaj głos.

jimhooker2002
źródło
1
Reszta strony, do której utworzyłeś link, wyjaśnia to: Podobnie jak w przypadku redukuj (), zaletą wyrażania kolekcjonowania w ten abstrakcyjny sposób jest to, że można je bezpośrednio zrównoleglać: możemy równolegle gromadzić częściowe wyniki, a następnie łączyć je, o ile funkcje akumulacji i łączenia spełniają odpowiednie wymagania.
JB Nizet
1
zobacz także „Streams in Java 8: Reduce vs. Collect” Angeliki Langer - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

Odpowiedzi:

115

reducejest operacją „ zwijania ”, stosuje operator binarny do każdego elementu w strumieniu, gdzie pierwszy argument operatora jest wartością zwracaną przez poprzednią aplikację, a drugi argument jest bieżącym elementem strumienia.

collectjest operacją agregacji, w której tworzona jest „kolekcja”, a każdy element jest „dodawany” do tej kolekcji. Zbiory w różnych częściach strumienia są następnie sumowane.

Dokument powiązany daje powód mający dwa różne podejścia:

Gdybyśmy chcieli pobrać strumień ciągów i połączyć je w jeden długi ciąg, moglibyśmy to osiągnąć zwykłą redukcją:

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

Uzyskalibyśmy pożądany rezultat, a nawet działałby równolegle. Jednak możemy nie być zadowoleni z wydajności! Taka implementacja wymagałaby kopiowania wielu ciągów, a czas wykonywania wyniósłby O (n ^ 2) w liczbie znaków. Bardziej wydajnym podejściem byłoby gromadzenie wyników w StringBuilder, który jest zmiennym kontenerem do gromadzenia ciągów. Możemy użyć tej samej techniki do zrównoleglenia redukcji mutowalnej, jak robimy to w przypadku zwykłej redukcji.

Chodzi więc o to, że zrównoleglenie jest takie samo w obu przypadkach, ale w tym reduceprzypadku stosujemy funkcję do samych elementów strumienia. W collectprzypadku, gdy zastosujemy funkcję do zmiennego kontenera.

Pająk Boris
źródło
1
Jeśli tak jest w przypadku metody collect: „Bardziej wydajnym podejściem byłoby gromadzenie wyników w StringBuilder”, to dlaczego mielibyśmy kiedykolwiek używać redukuj?
jimhooker2002
2
@ Jimhooker2002 ponownie go przeczytać. Jeśli, powiedzmy, obliczasz iloczyn, to funkcja redukcji może być po prostu zastosowana do podzielonych strumieni równolegle, a następnie połączona razem na końcu. Proces redukcji zawsze powoduje, że typ jest strumieniem. Zbieranie jest używane, gdy chcesz zebrać wyniki do zmiennego kontenera, tj. Gdy wynik jest innego typu niż strumień. Ma to tę zaletę, że dla każdego podzielonego strumienia można użyć jednego egzemplarza pojemnika, ale ma tę wadę, że pojemniki muszą być połączone na końcu.
Boris the Spider
1
@ jimhooker2002 w przykładzie produktu intjest niezmienna, więc nie można łatwo użyć operacji zbierania. Możesz zrobić nieprzyzwoity hack, taki jak użycie AtomicIntegerniestandardowego lub jakiegoś niestandardowego, IntWrapperale dlaczego miałbyś to zrobić? Operacja składania różni się po prostu od operacji zbierania.
Boris the Spider
17
Istnieje również inna reducemetoda, w której można zwrócić obiekty o typie innym niż elementy strumienia.
damluar
1
Jeszcze jeden przypadek, w którym u użyłby zbierania zamiast redukcji, to sytuacja, w której operacja redukcji polega na dodaniu elementów do kolekcji, a następnie za każdym razem, gdy funkcja akumulatora przetwarza element, tworzy nową kolekcję zawierającą element, który jest nieefektywny.
raghu
40

Powód jest prosty:

  • collect() może działać tylko ze zmiennymi obiektami wyników.
  • reduce()jest przeznaczony do pracy z niezmiennymi obiektami wynikowymi.

reduce()Przykład „ z niezmiennym”

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()Przykład „ ze zmienną”

Na przykład, jeśli chcesz ręcznie obliczyć sumę za pomocą collect()nie może pracować z BigDecimalale tylko MutableIntz org.apache.commons.lang.mutablenp. Widzieć:

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

Działa to, ponieważ akumulator container.add(employee.getSalary().intValue()); nie powinien zwracać nowego obiektu z wynikiem, ale zmieniać stan mutowalnego containertypu MutableInt.

Jeśli BigDecimalzamiast tego containerchcesz użyć collect()metody container.add(employee.getSalary());, nie możesz użyć metody, ponieważ nie zmieni to, containerponieważ BigDecimaljest niezmienna. (Poza tym BigDecimal::newnie zadziała, ponieważ BigDecimalnie ma pustego konstruktora)

Sandro
źródło
2
Zauważ, że używasz Integerkonstruktora ( new Integer(6)), który jest przestarzały w późniejszych wersjach Java.
MC Emperor
1
Dobry chwyt @MCEmperor! Zmieniłem to naInteger.valueOf(6)
Sandro
@Sandro - Jestem zdezorientowany. Dlaczego mówisz, że metoda collect () działa tylko z obiektami zmiennymi? Użyłem go do łączenia łańcuchów. String allNames = workers.stream () .map (Employee :: getNameString) .collect (Collectors.joining (",")) .toString ();
MasterJoe 2
1
@ MasterJoe2 To proste. Krótko mówiąc - implementacja nadal korzysta z tego, StringBuilderktóry jest zmienny. Zobacz: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
Sandro
30

Normalna redukcja ma na celu połączenie dwóch niezmiennych wartości, takich jak int, double itp., I utworzenie nowej; to niezmienna redukcja. W przeciwieństwie do tego metoda zbierania została zaprojektowana w celu zmutowania pojemnika w celu zgromadzenia wyniku, który ma przynieść.

Aby zilustrować problem, załóżmy, że chcesz osiągnąć Collectors.toList()za pomocą prostej redukcji, takiej jak

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

To jest odpowiednik Collectors.toList(). Jednak w tym przypadku modyfikujesz plik List<Integer>. Jak wiemy ArrayList, nie jest bezpieczny dla wątków, ani nie można bezpiecznie dodawać / usuwać z niego wartości podczas iteracji, więc otrzymasz wyjątek współbieżny ArrayIndexOutOfBoundsExceptionlub dowolny inny wyjątek (szczególnie gdy jest uruchamiany równolegle) podczas aktualizowania listy lub sumatora próbuje scalić listy, ponieważ modyfikujesz listę przez gromadzenie (dodawanie) do niej liczb całkowitych. Jeśli chcesz, aby ten wątek był bezpieczny, musisz za każdym razem przekazywać nową listę, która wpłynie negatywnie na wydajność.

Natomiast Collectors.toList()działa w podobny sposób. Jednak gwarantuje bezpieczeństwo wątków, gdy gromadzisz wartości na liście. Z dokumentacji collectmetody :

Wykonuje mutowalną operację redukcji na elementach tego strumienia przy użyciu Collectora. Jeśli strumień jest równoległy, a moduł zbierający jest współbieżny, a strumień jest nieuporządkowany lub kolektor jest nieuporządkowany, zostanie przeprowadzona współbieżna redukcja. W przypadku wykonywania równoległego można utworzyć wystąpienie, zapełnić i scalić wiele wyników pośrednich, aby zachować izolację zmiennych struktur danych. Dlatego nawet w przypadku wykonywania równolegle ze strukturami danych, które nie są bezpieczne dla wątków (takimi jak ArrayList), nie jest wymagana dodatkowa synchronizacja do równoległej redukcji.

Więc odpowiadając na twoje pytanie:

Kiedy użyjesz collect()vs reduce()?

jeśli mają wartości niezmienne, takie jak ints, doubles, Stringsdziała wtedy normalnie redukcja dobrze. Jednakże, jeśli masz do reduceswoich wartości powiedzmy a List(zmienna struktura danych), musisz użyć mutowalnej redukcji z tą collectmetodą.

Jerzy
źródło
Myślę, że problem we fragmencie kodu polega na tym, że zajmie on tożsamość (w tym przypadku pojedynczą instancję tablicy ArrayList) i przyjmie, że jest ona „niezmienna”, aby mogli rozpocząć xwątki, każdy „dodając do tożsamości”, a następnie łącząc się razem. Dobry przykład.
rogerdpack
dlaczego mielibyśmy uzyskać wyjątek jednoczesnej modyfikacji, wywołanie strumieni jest po prostu ponownym uruchomieniem strumienia szeregowego, a co oznacza, że ​​będzie on przetwarzany przez pojedynczy wątek, a funkcja sumatora nie jest w ogóle wywoływana?
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); } }Próbowałem i nie dostałem wyjątku CCm
amarnath
@amarnathharish problem występuje, gdy próbujesz uruchomić go równolegle i wiele wątków próbuje uzyskać dostęp do tej samej listy
george
11

Niech strumień będzie a <- b <- c <- d

W redukcji,

będziesz miał ((a # b) # c) # d

gdzie # jest interesującą operacją, którą chciałbyś wykonać.

W kolekcji

Twój kolekcjoner będzie miał jakąś strukturę zbierającą K.

K konsumuje. K następnie zużywa b. K następnie zużywa c. K następnie zużywa d.

Na koniec pytasz K, jaki jest ostateczny wynik.

K następnie daje ci go.

Yan Ng
źródło
2

Są one bardzo różne w potencjalnej zużycie pamięci podczas wykonywania. Podczas gdy collect()zbiera i umieszcza wszystkie dane w kolekcji, reduce()jawnie prosi o określenie, w jaki sposób zmniejszyć dane, które przeszły przez strumień.

Na przykład, jeśli chcesz odczytać niektóre dane z pliku, przetworzyć je i umieścić w jakiejś bazie danych, możesz otrzymać kod strumienia java podobny do tego:

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

W tym przypadku używamy, collect()aby wymusić na Javie strumieniowe przesyłanie danych i zapisanie wyniku w bazie danych. Bez collect()danych nigdy nie jest odczytywane i nigdy nie są przechowywane.

Ten kod szczęśliwie generuje java.lang.OutOfMemoryError: Java heap spacebłąd w czasie wykonywania, jeśli rozmiar pliku jest wystarczająco duży lub rozmiar sterty jest wystarczająco mały. Oczywistym powodem jest to, że próbuje ułożyć wszystkie dane, które przeszły przez strumień (i faktycznie zostały już zapisane w bazie danych) w wynikowej kolekcji, co powoduje wysadzenie stosu.

Jednakże, jeśli zastąpi collect()się reduce()- to nie będzie już problemem, jak ten ostatni zmniejszy i odrzucić wszystkie dane, które uczyniły go przez.

W przedstawionym przykładzie wystarczy zamienić na collect()coś z reduce:

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

Nie musisz nawet dbać o to, aby obliczenia zależały od resultjęzyka, ponieważ Java nie jest czystym językiem FP (programowania funkcjonalnego) i nie możesz zoptymalizować danych, które nie są używane na dole strumienia z powodu możliwych efektów ubocznych .

averasko
źródło
3
Jeśli nie dbasz o wyniki zapisywania bazy danych, powinieneś użyć forEach ... nie musisz używać redukuj. Chyba że było to w celach ilustracyjnych.
DaveEdelstein
2

Oto przykład kodu

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

Oto wynik wykonania:

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

Funkcja Reduce obsługuje dwa parametry, pierwszy parametr jest poprzednią wartością zwracaną w strumieniu, drugi parametr jest bieżącą wartością obliczaną w strumieniu, sumuje pierwszą wartość i bieżącą wartość jako pierwszą wartość w następnym obliczeniu.

JetQin
źródło
0

Według docs

Kolektory redukujące () są najbardziej użyteczne, gdy są używane w wielopoziomowej redukcji, poniżej groupingBy lub partitioningBy. Aby wykonać prostą redukcję strumienia, użyj zamiast tego Stream.reduce (BinaryOperator).

Więc zasadniczo używałbyś reducing()tylko wtedy, gdy zostałeś zmuszony do zbierania. Oto kolejny przykład :

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

Zgodnie z tym poradnikiem redukcja jest czasami mniej wydajna

Operacja redukcji zawsze zwraca nową wartość. Jednak funkcja akumulatora zwraca również nową wartość za każdym razem, gdy przetwarza element strumienia. Załóżmy, że chcesz zredukować elementy strumienia do bardziej złożonego obiektu, takiego jak kolekcja. Może to utrudnić działanie Twojej aplikacji. Jeśli operacja redukcji obejmuje dodawanie elementów do kolekcji, to za każdym razem, gdy funkcja akumulatora przetwarza element, tworzy nową kolekcję zawierającą element, który jest nieefektywny. Zamiast tego bardziej wydajne byłoby zaktualizowanie istniejącej kolekcji. Możesz to zrobić za pomocą metody Stream.collect, którą opisano w następnej sekcji ...

Tak więc tożsamość jest „ponownie wykorzystywana” w scenariuszu redukcji, więc .reducejeśli to możliwe , jest nieco bardziej wydajna .

rogerdpack
źródło