Saya kesulitan memahami sepenuhnya peran yang combiner
dipenuhi dalam reduce
metode Streams .
Misalnya, kode berikut tidak dapat dikompilasi:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());
Kesalahan kompilasi mengatakan: (argumen mismatch; int tidak dapat dikonversi ke java.lang.String)
tetapi kode ini mengkompilasi:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(),
(accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);
Saya memahami bahwa metode penggabung digunakan dalam aliran paralel - jadi dalam contoh saya itu menambahkan bersama dua int terakumulasi menengah.
Tapi saya tidak mengerti mengapa contoh pertama tidak dapat dikompilasi tanpa penggabung atau bagaimana penggabung menyelesaikan konversi string ke int karena ini hanya menambahkan bersama dua int.
Adakah yang bisa menjelaskan ini?
java
java-8
java-stream
Louise Miller
sumber
sumber
Jawaban:
Dua dan tiga versi argumen
reduce
yang Anda coba gunakan tidak menerima tipe yang sama untukaccumulator
.Kedua argumen
reduce
tersebut didefinisikan sebagai :T reduce(T identity, BinaryOperator<T> accumulator)
Dalam kasus Anda, T adalah String, jadi
BinaryOperator<T>
harus menerima dua argumen String dan mengembalikan String. Tapi Anda meneruskan ke int dan String, yang menghasilkan kesalahan kompilasi yang Anda dapatkan -argument mismatch; int cannot be converted to java.lang.String
. Sebenarnya, saya pikir melewatkan 0 sebagai nilai identitas juga salah di sini, karena String diharapkan (T).Juga perhatikan bahwa versi mengurangi proses aliran Ts dan mengembalikan T, jadi Anda tidak dapat menggunakannya untuk mengurangi aliran String ke int.
Ketiga argumen
reduce
tersebut didefinisikan sebagai :<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Dalam kasus Anda U adalah Integer dan T adalah String, jadi metode ini akan mengurangi aliran String menjadi Integer.
Untuk
BiFunction<U,? super T,U>
akumulator, Anda dapat mengirimkan parameter dari dua tipe berbeda (U dan? Super T), yang dalam kasus Anda adalah Integer dan String. Selain itu, nilai identitas U menerima Integer dalam kasus Anda, jadi meneruskannya dengan 0 tidak masalah.Cara lain untuk mencapai apa yang Anda inginkan:
int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) .reduce(0, (accumulatedInt, len) -> accumulatedInt + len);
Di sini jenis aliran cocok dengan jenis kembalian
reduce
, sehingga Anda dapat menggunakan dua versi parameterreduce
.Tentu saja Anda tidak harus menggunakan
reduce
sama sekali:int length = asList("str1", "str2").stream().mapToInt (s -> s.length()) .sum();
sumber
mapToInt(String::length)
lebihmapToInt(s -> s.length())
, tidak yakin apakah salah satu lebih baik daripada yang lain, tetapi saya lebih suka yang pertama untuk keterbacaan.combiner
diperlukan, mengapa tidak memilikiaccumulator
sudah cukup. Dalam hal ini: Penggabung hanya diperlukan untuk aliran paralel, untuk menggabungkan hasil "akumulasi" dari utas.Jawaban Eran menjelaskan perbedaan antara versi dua-argumen dan tiga-argumen
reduce
dalam yang pertama direduksiStream<T>
menjadiT
sedangkan yang terakhir dikurangiStream<T>
menjadiU
. Namun, itu tidak benar-benar menjelaskan perlunya fungsi penggabung tambahan saat mengurangiStream<T>
keU
.Salah satu prinsip desain Streams API adalah bahwa API tidak boleh berbeda antara aliran sekuensial dan paralel, atau dengan kata lain, API tertentu tidak boleh mencegah aliran berjalan dengan benar baik secara berurutan atau paralel. Jika lambda Anda memiliki properti yang benar (asosiatif, tidak mengganggu, dll.), Aliran yang dijalankan secara berurutan atau paralel akan memberikan hasil yang sama.
Mari pertama-tama pertimbangkan versi reduksi dua argumen:
T reduce(I, (T, T) -> T)
Implementasi sekuensial sangat mudah. Nilai identitas
I
"diakumulasikan" dengan elemen aliran nol untuk memberikan hasil. Hasil ini diakumulasikan dengan elemen aliran pertama untuk memberikan hasil lain, yang pada gilirannya diakumulasikan dengan elemen aliran kedua, dan seterusnya. Setelah elemen terakhir diakumulasikan, hasil akhirnya dikembalikan.Penerapan paralel dimulai dengan membagi aliran menjadi beberapa segmen. Setiap segmen diproses oleh utasnya sendiri secara berurutan yang saya jelaskan di atas. Sekarang, jika kami memiliki N utas, kami memiliki hasil menengah N. Ini perlu dikurangi menjadi satu hasil. Karena setiap hasil perantara berjenis T, dan kita memiliki beberapa, kita dapat menggunakan fungsi akumulator yang sama untuk mengurangi hasil perantara N tersebut menjadi satu hasil.
Sekarang mari kita pertimbangkan operasi pengurangan dua argumen hipotetis yang direduksi
Stream<T>
menjadiU
. Dalam bahasa lain, ini disebut operasi "lipat" atau "lipat kiri" jadi saya akan menyebutnya di sini. Perhatikan bahwa ini tidak ada di Java.U foldLeft(I, (U, T) -> U)
(Perhatikan bahwa nilai identitas
I
adalah tipe U.)Versi sekuensial dari
foldLeft
sama seperti versi sekuensialreduce
kecuali bahwa nilai tengahnya adalah tipe U dan bukan tipe T. Tapi sebaliknya sama. (foldRight
Operasi hipotetis akan serupa kecuali bahwa operasi akan dilakukan dari kanan ke kiri, bukan dari kiri ke kanan.)Sekarang perhatikan versi paralel dari
foldLeft
. Mari kita mulai dengan membagi arus menjadi beberapa segmen. Kemudian kita dapat meminta setiap benang N mengurangi nilai T di segmennya menjadi nilai antara N tipe U. Sekarang apa? Bagaimana kita mendapatkan dari nilai N tipe U ke hasil tunggal tipe U?Apa yang hilang adalah fungsi lain yang menggabungkan beberapa hasil antara tipe U menjadi satu hasil tipe U. Jika kita memiliki fungsi yang menggabungkan dua nilai U menjadi satu, itu cukup untuk mengurangi sejumlah nilai menjadi satu - seperti pengurangan asli di atas. Dengan demikian, operasi reduksi yang memberikan hasil dari tipe yang berbeda membutuhkan dua fungsi:
U reduce(I, (U, T) -> U, (U, U) -> U)
Atau, menggunakan sintaks Java:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Singkatnya, untuk melakukan reduksi paralel ke jenis hasil yang berbeda, kita memerlukan dua fungsi: satu yang mengakumulasi elemen T ke nilai U perantara, dan yang kedua yang menggabungkan nilai U perantara menjadi satu hasil U. Jika kita tidak berpindah tipe, ternyata fungsi akumulatornya sama dengan fungsi penggabung. Itulah mengapa reduksi ke jenis yang sama hanya memiliki fungsi akumulator dan reduksi ke jenis yang berbeda memerlukan fungsi akumulator dan penggabung yang terpisah.
Akhirnya, Java tidak menyediakan
foldLeft
danfoldRight
operasi karena mereka menyiratkan pemesanan tertentu operasi yang secara inheren berurutan. Ini bertentangan dengan prinsip desain yang disebutkan di atas dalam menyediakan API yang mendukung operasi sekuensial dan paralel secara setara.sumber
foldLeft
karena perhitungannya bergantung pada hasil sebelumnya dan tidak dapat diparalelkan?forEachOrdered
. Status perantara harus disimpan dalam variabel yang ditangkap.foldLeft
rapi .Karena saya suka corat-coret dan panah untuk mengklarifikasi konsep ... ayo mulai!
Dari String ke String (aliran berurutan)
Misalkan memiliki 4 string: tujuan Anda adalah menggabungkan string tersebut menjadi satu. Anda pada dasarnya memulai dengan satu tipe dan menyelesaikannya dengan tipe yang sama.
Anda dapat mencapai ini dengan
String res = Arrays.asList("one", "two","three","four") .stream() .reduce("", (accumulatedStr, str) -> accumulatedStr + str); //accumulator
dan ini membantu Anda memvisualisasikan apa yang terjadi:
Fungsi akumulator mengubah, selangkah demi selangkah, elemen dalam aliran (merah) Anda menjadi nilai akhir yang dikurangi (hijau). Fungsi akumulator hanya mengubah suatu
String
objek menjadi objek lainString
.Dari String ke int (aliran paralel)
Misalkan memiliki 4 string yang sama: tujuan baru Anda adalah menjumlahkan panjangnya, dan Anda ingin memparalelkan aliran Anda.
Yang Anda butuhkan adalah sesuatu seperti ini:
int length = Arrays.asList("one", "two","three","four") .parallelStream() .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length(), //accumulator (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner
dan ini adalah skema dari apa yang terjadi
Di sini, fungsi akumulator (a
BiFunction
) memungkinkan Anda mengubahString
data menjadiint
data. Menjadi aliran paralel, itu terbagi menjadi dua bagian (merah), yang masing-masing diuraikan secara independen satu sama lain dan menghasilkan hasil parsial (oranye) yang sama banyaknya. Mendefinisikan penggabung diperlukan untuk memberikan aturan untuk menggabungkanint
hasil parsial menjadi hasil akhir (hijau)int
.Dari String ke int (aliran berurutan)
Bagaimana jika Anda tidak ingin memparalelkan aliran Anda? Yah, bagaimanapun, penggabung perlu disediakan, tetapi itu tidak akan pernah dipanggil, mengingat tidak ada hasil parsial yang akan dihasilkan.
sumber
Tidak ada versi pengurangan yang mengambil dua jenis berbeda tanpa penggabung karena tidak dapat dijalankan secara paralel (tidak yakin mengapa ini menjadi persyaratan). Fakta bahwa akumulator harus asosiatif membuat antarmuka ini tidak berguna karena:
Menghasilkan hasil yang sama seperti:
sumber
map
Trik seperti itu tergantung pada khususnyaaccumulator
dancombiner
dapat memperlambat banyak hal.accumulator
dengan menghapus parameter pertama.