Mam problem z pełnym zrozumieniem roli, jaką combiner
spełnia reduce
metoda strumieniowa .
Na przykład następujący kod nie kompiluje się:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());
Komunikat o błędzie kompilacji: (niezgodność argumentów; nie można przekonwertować wartości int na java.lang.String)
ale ten kod się kompiluje:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(),
(accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);
Rozumiem, że metoda sumatora jest używana w równoległych strumieniach - więc w moim przykładzie jest to sumowanie dwóch pośrednich skumulowanych int.
Ale nie rozumiem, dlaczego pierwszy przykład nie kompiluje się bez sumatora lub w jaki sposób sumator rozwiązuje konwersję ciągu znaków na int, ponieważ jest to po prostu dodanie dwóch liczb całkowitych.
Czy ktoś może rzucić na to światło?
java
java-8
java-stream
Louise Miller
źródło
źródło
Odpowiedzi:
Wersje dwu- i trzyargumentowe,
reduce
których próbowano użyć, nie akceptują tego samego typu dlaaccumulator
.Te dwa argumenty
reduce
są zdefiniowane jako :W twoim przypadku T jest Stringiem, więc
BinaryOperator<T>
powinno przyjąć dwa argumenty typu String i zwrócić String. Ale przekazujesz do niego int i String, co powoduje błąd kompilacji -argument mismatch; int cannot be converted to java.lang.String
. Właściwie myślę, że przekazywanie 0, ponieważ wartość tożsamości jest również tutaj błędne, ponieważ oczekuje się String (T).Zauważ również, że ta wersja redukuje przetwarza strumień Ts i zwraca T, więc nie możesz jej użyć do zredukowania strumienia String do int.
Te trzy argumenty
reduce
są zdefiniowane jako :W twoim przypadku U to Integer, a T to String, więc ta metoda zredukuje strumień String do Integer.
Dla
BiFunction<U,? super T,U>
akumulatora można przekazać parametry dwóch różnych typów (U i? Super T), którymi w twoim przypadku są Integer i String. Ponadto wartość tożsamości U akceptuje w twoim przypadku liczbę całkowitą, więc przekazanie jej 0 jest w porządku.Inny sposób na osiągnięcie tego, co chcesz:
Tutaj typ strumienia pasuje do zwracanego typu
reduce
, więc możesz użyć wersji z dwoma parametramireduce
.Oczywiście nie musisz
reduce
w ogóle używać :źródło
mapToInt(String::length)
overmapToInt(s -> s.length())
, nie jestem pewien, czy jeden byłby lepszy od drugiego, ale wolę ten pierwszy ze względu na czytelność.combiner
jest to potrzebne, dlaczego nieaccumulator
wystarczy. W takim przypadku: Łącznik jest potrzebny tylko dla równoległych strumieni, aby połączyć „nagromadzone” wyniki wątków.Odpowiedź Erana opisuje różnice między wersjami dwuargumentowymi i trójargumentowymi
reduce
w tym, że pierwsza redukuje sięStream<T>
do,T
podczas gdy drugaStream<T>
doU
. Jednak w rzeczywistości nie wyjaśnia to potrzeby dodatkowej funkcji sumatora podczas redukcjiStream<T>
doU
.Jedną z zasad projektowych interfejsu Streams API jest to, że interfejs API nie powinien różnić się między strumieniami sekwencyjnymi i równoległymi lub inaczej mówiąc, określony interfejs API nie powinien uniemożliwiać poprawnego działania strumienia sekwencyjnie lub równolegle. Jeśli Twoje lambdy mają odpowiednie właściwości (asocjacyjne, niezakłócające itp.), Strumień uruchamiany sekwencyjnie lub równolegle powinien dawać te same wyniki.
Najpierw rozważmy dwuargumentową wersję redukcji:
Implementacja sekwencyjna jest prosta. Wartość tożsamości
I
jest „kumulowana” z zerowym elementem stream w celu uzyskania wyniku. Wynik ten jest gromadzony z pierwszym elementem strumienia, aby dać inny wynik, który z kolei jest gromadzony z drugim elementem strumienia i tak dalej. Po skumulowaniu ostatniego elementu zwracany jest wynik końcowy.Wdrożenie równoległe rozpoczyna się od podzielenia strumienia na segmenty. Każdy segment jest przetwarzany przez swój własny wątek w sposób sekwencyjny, który opisałem powyżej. Teraz, jeśli mamy N wątków, mamy N wyników pośrednich. Należy je zredukować do jednego wyniku. Ponieważ każdy wynik pośredni jest typu T, a mamy ich kilka, możemy użyć tej samej funkcji akumulatora, aby zredukować te wyniki pośrednie N do jednego wyniku.
Rozważmy teraz hipotetyczną dwuargumentową operację redukcji, która sprowadza się
Stream<T>
doU
. W innych językach nazywa się to operacją „zagięcia” lub „zagięcia w lewo”, więc tak to tutaj nazywam. Zauważ, że to nie istnieje w Javie.(Zwróć uwagę, że wartość tożsamości
I
jest typu U.)Sekwencyjna wersja programu
foldLeft
jest taka sama, jak sekwencyjna wersja programu,reduce
z tą różnicą, że wartości pośrednie są typu U zamiast typu T. Poza tym jest to to samo. (HipotetycznafoldRight
operacja byłaby podobna, z tą różnicą, że operacje byłyby wykonywane od prawej do lewej zamiast od lewej do prawej).Rozważmy teraz równoległą wersję
foldLeft
. Zacznijmy od podzielenia strumienia na segmenty. Możemy zatem sprawić, że każdy z N wątków zredukuje wartości T w swoim segmencie do N pośrednich wartości typu U. I co teraz? Jak uzyskać od N wartości typu U do pojedynczego wyniku typu U?Brakuje innej funkcji, która łączy wiele wyników pośrednich typu U w jeden wynik typu U. Jeśli mamy funkcję, która łączy dwie wartości U w jedną, to wystarczy, aby zredukować dowolną liczbę wartości do jednej - tak jak oryginalna redukcja powyżej. Zatem operacja redukcji, która daje wynik innego typu, wymaga dwóch funkcji:
Lub używając składni Java:
Podsumowując, aby przeprowadzić równoległą redukcję do innego typu wyniku, potrzebujemy dwóch funkcji: jednej, która gromadzi elementy T do pośrednich wartości U, i drugiej, która łączy pośrednie wartości U w jeden wynik U. Jeśli nie zmieniamy typów, okazuje się, że funkcja akumulatora jest taka sama jak funkcja sumatora. Dlatego redukcja do tego samego typu ma tylko funkcję akumulatora, a redukcja do innego typu wymaga oddzielnych funkcji akumulatora i sumatora.
Wreszcie, Java nie udostępnia operacji
foldLeft
ifoldRight
, ponieważ implikują one określoną kolejność operacji, która jest z natury sekwencyjna. Jest to sprzeczne z zasadą projektowania opisaną powyżej, polegającą na zapewnianiu interfejsów API, które w równym stopniu obsługują operacje sekwencyjne i równoległe.źródło
foldLeft
ponieważ obliczenia zależą od poprzedniego wyniku i nie można ich zrównoleglać?forEachOrdered
. Stan pośredni należy jednak zachować w przechwyconej zmiennej.foldLeft
.Ponieważ lubię gryzmoły i strzałki, aby wyjaśnić koncepcje, zacznijmy!
Od ciągu do ciągu (strumień sekwencyjny)
Załóżmy, że masz 4 ciągi: Twoim celem jest połączenie takich ciągów w jeden. Zasadniczo zaczynasz od typu i kończysz tym samym typem.
Możesz to osiągnąć dzięki
a to pomaga w wizualizacji tego, co się dzieje:
Funkcja akumulatora konwertuje, krok po kroku, elementy w (czerwonym) strumieniu do końcowej zredukowanej (zielonej) wartości. Funkcja akumulatora po prostu przekształca
String
obiekt w innyString
.Od ciągu do int (strumień równoległy)
Załóżmy, że mamy te same 4 ciągi: Twoim nowym celem jest zsumowanie ich długości i chcesz zrównoleglenie strumienia.
Potrzebujesz czegoś takiego:
a to jest schemat tego, co się dzieje
Tutaj funkcja akumulatora (a
BiFunction
) umożliwia przekształcenieString
danych wint
dane. Ponieważ strumień jest równoległy, jest podzielony na dwie (czerwone) części, z których każda jest opracowywana niezależnie od siebie i daje tyle samo wyników częściowych (pomarańczowych). Zdefiniowanie sumatora jest potrzebne, aby zapewnić regułę scalaniaint
wyników częściowych w końcowy (zielony)int
.Od ciągu do int (strumień sekwencyjny)
A co jeśli nie chcesz zrównoleglać swojego strumienia? Cóż, i tak trzeba podać sumator, ale nigdy nie zostanie on wywołany, biorąc pod uwagę, że nie zostaną wytworzone żadne częściowe wyniki.
źródło
Nie ma wersji redukującej, która przyjmuje dwa różne typy bez sumatora, ponieważ nie można jej wykonać równolegle (nie wiem, dlaczego jest to wymagane). Fakt, że akumulator musi być asocjacyjny, sprawia, że ten interfejs jest praktycznie bezużyteczny, ponieważ:
Daje takie same wyniki jak:
źródło
map
sztuczka zależy od konkretnej sytuacjiaccumulator
icombiner
może znacznie spowolnić działanie.accumulator
, pomijając pierwszy parametr.