Java 8 Streaming - kumpulkan vs kurangi

143

Kapan Anda akan menggunakan collect()vs reduce()? Adakah yang punya contoh konkret yang baik tentang kapan lebih baik pergi ke satu arah atau yang lain?

Javadoc menyebutkan bahwa mengumpulkan () adalah pengurangan yang bisa berubah .

Mengingat bahwa ini adalah pengurangan yang bisa berubah, saya menganggap itu memerlukan sinkronisasi (secara internal) yang, pada gilirannya, dapat merusak kinerja. Agaknya reduce()lebih mudah diparalelkan dengan biaya harus membuat struktur data baru untuk kembali setelah setiap langkah dalam pengurangan.

Pernyataan di atas hanyalah dugaan dan saya ingin ahli untuk berpadu di sini.

jimhooker2002
sumber
1
Sisa halaman yang Anda tautkan menjelaskannya: Seperti halnya dengan pengurangan (), manfaat dari pengekspektan kumpulkan dengan cara abstrak ini adalah bahwa ia langsung dapat diterima untuk paralelisasi: kami dapat mengumpulkan hasil parsial secara paralel dan kemudian menggabungkannya, selama fungsi akumulasi dan kombinasi memenuhi persyaratan yang sesuai.
JB Nizet
1
juga lihat "Streaming di Jawa 8: Kurangi vs Kumpulkan" oleh Angelika Langer - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

Jawaban:

115

reduceadalah operasi " lipat ", itu berlaku operator biner untuk setiap elemen dalam aliran di mana argumen pertama ke operator adalah nilai kembali dari aplikasi sebelumnya dan argumen kedua adalah elemen aliran saat ini.

collectadalah operasi agregasi di mana "koleksi" dibuat dan setiap elemen "ditambahkan" ke koleksi itu. Koleksi di berbagai bagian aliran kemudian ditambahkan bersama.

The dokumen Anda terhubung memberi alasan untuk memiliki dua pendekatan yang berbeda:

Jika kami ingin mengambil aliran string dan menggabungkannya menjadi satu string panjang, kami dapat mencapai ini dengan reduksi biasa:

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

Kami akan mendapatkan hasil yang diinginkan, dan bahkan akan bekerja secara paralel. Namun, kami mungkin tidak senang dengan kinerjanya! Implementasi seperti itu akan melakukan banyak penyalinan string, dan run time akan menjadi O (n ^ 2) dalam jumlah karakter. Pendekatan yang lebih performan adalah dengan mengakumulasi hasil menjadi StringBuilder, yang merupakan wadah yang bisa berubah untuk mengakumulasi string. Kita bisa menggunakan teknik yang sama untuk memaralisasi reduksi yang bisa berubah seperti yang kita lakukan dengan reduksi biasa.

Jadi intinya adalah bahwa parallelisation adalah sama dalam kedua kasus tetapi dalam reducekasus ini kita menerapkan fungsi ke elemen aliran itu sendiri. Dalam hal collectini kita menerapkan fungsi ke wadah yang bisa berubah.

Boris the Spider
sumber
1
Jika ini adalah kasus untuk kumpulkan: "Pendekatan yang lebih berkinerja adalah untuk mengakumulasi hasil menjadi StringBuilder" lalu mengapa kita menggunakan pengurangan?
jimhooker2002
2
@ Jimhooker2002 membacanya kembali. Jika Anda, katakanlah, menghitung produk maka fungsi reduksi dapat dengan mudah diterapkan pada aliran terpisah secara paralel dan kemudian digabungkan bersama di akhir. Proses mengurangi selalu menghasilkan jenis sebagai aliran. Mengumpulkan digunakan ketika Anda ingin mengumpulkan hasil ke wadah yang bisa berubah, yaitu ketika hasilnya berbeda jenis ke aliran. Ini memiliki keuntungan bahwa satu contoh wadah dapat digunakan untuk masing-masing aliran terpisah tetapi kerugian bahwa wadah perlu dikombinasikan di akhir.
Boris the Spider
1
@ jimhooker2002 dalam contoh produk, inttidak dapat diubah sehingga Anda tidak dapat dengan mudah menggunakan operasi pengumpulan. Anda bisa melakukan peretasan kotor seperti menggunakan AtomicIntegeratau beberapa kebiasaan IntWrappertetapi mengapa Anda melakukannya? Operasi lipatan sangat berbeda dengan operasi pengumpulan.
Boris the Spider
17
Ada juga reducemetode lain , di mana Anda bisa mengembalikan objek bertipe berbeda dari elemen stream.
damluar
1
satu lagi kasus di mana Anda akan menggunakan kumpulkan alih-alih mengurangi adalah ketika mengurangi operasi melibatkan menambahkan elemen ke koleksi, maka setiap kali fungsi akumulator Anda memproses elemen, itu menciptakan koleksi baru yang mencakup elemen, yang tidak efisien.
raghu
40

Alasannya sederhana:

  • collect() hanya dapat bekerja dengan objek hasil yang bisa berubah .
  • reduce()adalah dirancang untuk bekerja dengan berubah benda hasil.

" reduce()dengan abadi" contoh

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()Contoh " dengan bisa berubah"

Misal, jika Anda ingin menghitung jumlah secara manual, menggunakannya collect()tidak dapat bekerja dengan BigDecimaltetapi hanya dengan MutableIntdari org.apache.commons.lang.mutablemisalnya. Lihat:

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

Ini bekerja karena akumulator container.add(employee.getSalary().intValue()); tidak seharusnya mengembalikan objek baru dengan hasil tetapi untuk mengubah keadaan bisa berubah containerdari jenis MutableInt.

Jika Anda ingin menggunakan BigDecimalsebagai gantinya containerAnda tidak dapat menggunakan collect()metode karena container.add(employee.getSalary());tidak akan mengubah containerkarena BigDecimaltidak dapat diubah. (Terlepas dari ini BigDecimal::newtidak akan berfungsi karena BigDecimaltidak memiliki konstruktor kosong)

Sandro
sumber
2
Perhatikan bahwa Anda menggunakan Integerkonstruktor ( new Integer(6)), yang tidak digunakan lagi di versi Java yang lebih baru.
MC Emperor
1
Tangkapan yang bagus @MCEmperor! Saya telah mengubahnya keInteger.valueOf(6)
Sandro
@ Carlos - Saya bingung. Mengapa Anda mengatakan bahwa collect () hanya bekerja dengan objek yang bisa berubah? Saya menggunakannya untuk menyatukan string. String allNames = employee.stream () .map (Employee :: getNameString) .collect (Collectors.joining (",")) .toString ();
MasterJoe2
1
@ MasterJoe2 Sederhana. Singkatnya - implementasi masih menggunakan StringBuilderyang bisa berubah. Lihat: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
Sandro
30

Reduksi normal dimaksudkan untuk menggabungkan dua nilai yang tidak berubah seperti int, dobel, dll. Dan menghasilkan yang baru; ini adalah pengurangan yang tidak berubah . Sebaliknya, metode kumpulkan dirancang untuk bermutasi wadah untuk mengakumulasikan hasil yang seharusnya dihasilkan.

Untuk mengilustrasikan masalah, misalkan Anda ingin mencapai Collectors.toList()menggunakan pengurangan sederhana seperti

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

Ini setara dengan Collectors.toList(). Namun, dalam hal ini Anda memutasikan List<Integer>. Seperti yang kita ketahui, ArrayListini bukan thread-safe, juga tidak aman untuk menambah / menghapus nilai dari itu saat iterasi sehingga Anda akan mendapatkan pengecualian bersamaan atau ArrayIndexOutOfBoundsExceptionatau segala jenis pengecualian (terutama ketika dijalankan secara paralel) ketika Anda memperbarui daftar atau penggabung mencoba untuk menggabungkan daftar karena Anda mengubah daftar dengan mengakumulasi (menambahkan) bilangan bulat ke dalamnya. Jika Anda ingin membuat utas ini aman, Anda harus memberikan daftar baru setiap kali yang akan mengganggu kinerja.

Sebaliknya, Collectors.toList()karya - karyanya serupa. Namun, itu menjamin keamanan utas saat Anda mengakumulasi nilai-nilai ke dalam daftar. Dari dokumentasi untuk collectmetode ini :

Melakukan operasi pengurangan yang bisa berubah pada elemen aliran ini menggunakan Kolektor. Jika aliran paralel, dan Kolektor bersamaan, dan aliran tidak berurutan atau kolektor tidak berurutan, maka pengurangan bersamaan akan dilakukan. Ketika dieksekusi secara paralel, beberapa hasil antara dapat dipakai, diisi, dan digabungkan untuk menjaga isolasi struktur data yang bisa berubah. Oleh karena itu, bahkan ketika dieksekusi secara paralel dengan struktur data yang tidak aman (seperti ArrayList), tidak diperlukan sinkronisasi tambahan untuk pengurangan paralel.

Jadi, untuk menjawab pertanyaanmu:

Kapan Anda akan menggunakan collect()vs reduce()?

jika Anda memiliki nilai-nilai abadi seperti ints, doubles, Stringsmaka pengurangan biasa bekerja dengan baik. Namun, jika Anda harus reducenilai-nilai Anda ke katakanlah List(struktur data bisa berubah) maka Anda perlu menggunakan pengurangan bisa berubah dengan collectmetode ini.

George
sumber
Dalam cuplikan kode saya pikir masalahnya adalah ia akan mengambil identitas (dalam hal ini satu contoh dari ArrayList) dan menganggap itu "tidak berubah" sehingga mereka dapat memulai xutas, masing-masing "menambah identitas" kemudian menggabungkan bersama. Contoh yang baik.
rogerdpack
mengapa kita akan mendapatkan pengecualian modifikasi bersamaan, memanggil stream hanya akan retun stream serial dan yang artinya akan diproses oleh satu thread dan fungsi combiner sama sekali tidak disebut?
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); } }saya mencoba dan tidak mendapatkan pengecualian CCm
amarnath harish
@amarnathharish masalah terjadi ketika Anda mencoba menjalankannya secara paralel dan beberapa utas mencoba mengakses daftar yang sama
george
11

Biarkan aliran menjadi <- b <- c <- d

Dalam pengurangan,

Anda akan memiliki ((a # b) # c) # d

di mana # adalah operasi yang menarik yang ingin Anda lakukan.

Dalam koleksi,

kolektor Anda akan memiliki semacam struktur pengumpulan K.

K mengkonsumsi a. K kemudian mengkonsumsi b. K kemudian mengkonsumsi c. K kemudian mengkonsumsi d.

Pada akhirnya, Anda bertanya pada K apa hasil akhirnya.

K kemudian memberikannya kepada Anda.

Yan Ng
sumber
2

Mereka sangat berbeda dalam jejak memori potensial selama runtime. Saat collect()mengumpulkan dan menempatkan semua data ke dalam koleksi, reduce()secara eksplisit meminta Anda untuk menentukan cara mengurangi data yang membuatnya melalui aliran.

Misalnya, jika Anda ingin membaca beberapa data dari file, memprosesnya, dan memasukkannya ke dalam database, Anda mungkin berakhir dengan kode aliran java yang mirip dengan ini:

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

Dalam hal ini, kami menggunakan collect()untuk memaksa java untuk melakukan streaming data dan membuatnya menyimpan hasilnya ke dalam database. Tanpa collect()data tidak pernah dibaca dan tidak pernah disimpan.

Kode ini dengan senang hati menghasilkan java.lang.OutOfMemoryError: Java heap spacekesalahan runtime, jika ukuran file cukup besar atau ukuran tumpukan cukup rendah. Alasan yang jelas adalah bahwa ia mencoba untuk menumpuk semua data yang membuatnya melalui aliran (dan, pada kenyataannya, telah disimpan dalam database) ke dalam koleksi yang dihasilkan dan ini memecah tumpukan.

Namun, jika Anda mengganti collect()dengan reduce()- itu tidak akan menjadi masalah lagi karena yang terakhir akan mengurangi dan membuang semua data yang berhasil melaluinya.

Dalam contoh yang disajikan, ganti saja collect()dengan sesuatu dengan reduce:

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

Anda bahkan tidak perlu peduli untuk membuat perhitungan tergantung pada resultJava bukan bahasa FP (pemrograman fungsional) murni dan tidak dapat mengoptimalkan data yang tidak digunakan di bagian bawah aliran karena kemungkinan efek samping .

averasko
sumber
3
Jika Anda tidak peduli dengan hasil penyimpanan db Anda, Anda harus menggunakan forEach ... Anda tidak perlu menggunakan pengurangan. Kecuali ini untuk tujuan ilustrasi.
DaveEdelstein
2

Ini adalah contoh kode

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

Inilah hasil eksekusi:

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

Mengurangi fungsi menangani dua parameter, parameter pertama adalah nilai pengembalian sebelumnya ke dalam aliran, parameter kedua adalah nilai penghitungan arus dalam arus, itu menjumlahkan nilai pertama dan nilai saat ini sebagai nilai pertama dalam perhitungan berikutnya.

JetQin
sumber
0

Menurut dokumen

Kolektor pereduksi () paling berguna ketika digunakan dalam pengurangan multi-level, hilir dari pengelompokan oleh atau partisi. Untuk melakukan pengurangan sederhana pada aliran, gunakan Stream.reduce (BinaryOperator) sebagai gantinya.

Jadi pada dasarnya Anda reducing()hanya akan menggunakannya saat dipaksa di dalam koleksi. Ini contoh lain :

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

Menurut tutorial ini mengurangi kadang-kadang kurang efisien

Operasi pengurangan selalu mengembalikan nilai baru. Namun, fungsi akumulator juga mengembalikan nilai baru setiap kali memproses elemen aliran. Misalkan Anda ingin mengurangi elemen aliran ke objek yang lebih kompleks, seperti koleksi. Ini mungkin menghambat kinerja aplikasi Anda. Jika operasi pengurangan Anda melibatkan penambahan elemen ke koleksi, maka setiap kali fungsi akumulator Anda memproses elemen, itu menciptakan koleksi baru yang menyertakan elemen, yang tidak efisien. Akan lebih efisien bagi Anda untuk memperbarui koleksi yang ada. Anda bisa melakukan ini dengan metode Stream.collect, yang dijelaskan di bagian selanjutnya ...

Jadi identitasnya "digunakan kembali" dalam skenario pengurangan, jadi sedikit lebih efisien untuk digunakan .reducejika memungkinkan.

rogerdpack
sumber