Apa perbedaan antara Collection.stream (). ForEach () dan Collection.forEach ()?


286

Saya mengerti bahwa dengan .stream(), saya dapat menggunakan operasi rantai seperti .filter()atau menggunakan aliran paralel. Tetapi apa perbedaan di antara mereka jika saya perlu menjalankan operasi kecil (misalnya, mencetak elemen dari daftar)?

collection.stream().forEach(System.out::println);
collection.forEach(System.out::println);

Jawaban:


287

Untuk kasus sederhana seperti yang diilustrasikan, sebagian besar sama. Namun, ada sejumlah perbedaan halus yang mungkin signifikan.

Salah satu masalah adalah dengan pemesanan. Dengan Stream.forEach, pesanan tidak ditentukan . Itu tidak mungkin terjadi dengan aliran berurutan, masih, itu dalam spesifikasi untuk Stream.forEachdieksekusi dalam beberapa urutan sewenang-wenang. Ini memang sering terjadi dalam aliran paralel. Sebaliknya, Iterable.forEachselalu dieksekusi dalam urutan iterasi Iterable, jika ada yang ditentukan.

Masalah lain adalah dengan efek samping. Tindakan yang ditentukan dalam Stream.forEachini harus tidak mengganggu . (Lihat dokumen paket java.util.stream .) Iterable.forEachBerpotensi memiliki pembatasan lebih sedikit. Untuk koleksi di java.util, Iterable.forEachumumnya akan menggunakan koleksi itu Iterator, yang sebagian besar dirancang untuk gagal-cepat dan yang akan melempar ConcurrentModificationExceptionjika koleksi tersebut secara struktural diubah selama iterasi. Namun, modifikasi yang tidak struktural yang diperbolehkan selama iterasi. Sebagai contoh, dokumentasi kelas ArrayList mengatakan "hanya menetapkan nilai elemen bukan modifikasi struktural." Demikianlah tindakan untukArrayList.forEachdiizinkan untuk menetapkan nilai di dasar ArrayListtanpa masalah.

Koleksi bersamaan lagi-lagi berbeda. Alih-alih gagal-cepat, mereka dirancang untuk konsisten lemah . Definisi lengkap ada di tautan itu. Namun, pertimbangkan secara singkat ConcurrentLinkedDeque. Tindakan diteruskan ke nya forEachmetode yang diperbolehkan untuk memodifikasi deque yang mendasari, bahkan struktural, dan ConcurrentModificationExceptiontidak pernah dilemparkan. Namun, modifikasi yang terjadi mungkin atau mungkin tidak terlihat dalam iterasi ini. (Karena itu konsistensi "lemah".)

Masih ada perbedaan lain yang terlihat jika Iterable.forEachiterasi pada koleksi yang disinkronkan. Pada koleksi seperti itu, Iterable.forEach ambil kunci koleksi sekali dan pegang semua panggilan ke metode tindakan. The Stream.forEachpanggilan menggunakan spliterator koleksi, yang tidak mengunci, dan yang bergantung pada aturan yang berlaku non-interferensi. Koleksi yang mendukung aliran dapat dimodifikasi selama iterasi, dan jika ya, ConcurrentModificationExceptionperilaku yang tidak konsisten dapat terjadi.


Iterable.forEach takes the collection's lock. Dari mana informasi ini berasal? Saya tidak dapat menemukan perilaku seperti itu di sumber JDK.
Turbanoff


@ Sittu, dapatkah Anda menguraikan non-campur. Stream.forEach () juga akan membuang ConcurrentModificationException (setidaknya untuk saya).
yuranos

1
@ yuranos87 Banyak koleksi seperti ArrayListmemiliki pemeriksaan yang cukup ketat untuk modifikasi bersamaan, dan karenanya akan sering melempar ConcurrentModificationException. Tetapi ini tidak dijamin, terutama untuk aliran paralel. Alih-alih CME Anda mungkin mendapatkan jawaban yang tidak terduga. Pertimbangkan juga modifikasi non-struktural terhadap sumber aliran. Untuk aliran paralel, Anda tidak tahu utas apa yang akan memproses elemen tertentu, atau apakah itu diproses pada saat itu dimodifikasi. Ini mengatur kondisi balapan, di mana Anda mungkin mendapatkan hasil yang berbeda di setiap lari, dan tidak pernah mendapatkan CME.
Stuart Marks

30

Jawaban ini berkaitan dengan kinerja berbagai implementasi loop. Hanya sedikit relevan untuk loop yang disebut SANGAT SERING (seperti jutaan panggilan). Dalam kebanyakan kasus, isi dari loop adalah elemen yang paling mahal. Untuk situasi di mana Anda sering melakukan loop, ini mungkin masih menarik.

Anda harus mengulang tes ini di bawah sistem target karena ini adalah implementasi khusus, ( kode sumber lengkap ).

Saya menjalankan openjdk versi 1.8.0_111 pada mesin Linux yang cepat.

Saya menulis tes yang mengulang 10 ^ 6 kali lebih dari Daftar menggunakan kode ini dengan berbagai ukuran untuk integers(10 ^ 0 -> 10 ^ 5 entri).

Hasilnya di bawah ini, metode tercepat bervariasi tergantung pada jumlah entri dalam daftar.

Tetapi masih dalam situasi terburuk, mengulang lebih dari 10 ^ 5 entri 10 ^ 6 kali butuh 100 detik untuk pemain terburuk, jadi pertimbangan lain lebih penting dalam hampir semua situasi.

public int outside = 0;

private void forCounter(List<Integer> integers) {
    for(int ii = 0; ii < integers.size(); ii++) {
        Integer next = integers.get(ii);
        outside = next*next;
    }
}

private void forEach(List<Integer> integers) {
    for(Integer next : integers) {
        outside = next * next;
    }
}

private void iteratorForEach(List<Integer> integers) {
    integers.forEach((ii) -> {
        outside = ii*ii;
    });
}
private void iteratorStream(List<Integer> integers) {
    integers.stream().forEach((ii) -> {
        outside = ii*ii;
    });
}

Berikut ini adalah timing saya: milidetik / fungsi / jumlah entri dalam daftar. Setiap run adalah 10 ^ 6 loop.

                           1    10    100    1000    10000
       iterator.forEach   27   116    959    8832    88958
               for:each   53   171   1262   11164   111005
         for with index   39   112    920    8577    89212
iterable.stream.forEach  255   324   1030    8519    88419

Jika Anda mengulangi percobaan, saya memposting kode sumber lengkap . Harap edit jawaban ini dan tambahkan hasil Anda dengan notasi sistem yang diuji.


Menggunakan MacBook Pro, 2,5 GHz Intel Core i7, 16 GB, macOS 10.12.6:

                           1    10    100    1000    10000
       iterator.forEach   27   106   1047    8516    88044
               for:each   46   143   1182   10548   101925
         for with index   49   145    887    7614    81130
iterable.stream.forEach  393   397   1108    8908    88361

Java 8 Hotspot VM - 3,4GHz Intel Xeon, 8 GB, Windows 10 Pro

                            1    10    100    1000    10000
        iterator.forEach   30   115    928    8384    85911
                for:each   40   125   1166   10804   108006
          for with index   30   120    956    8247    81116
 iterable.stream.forEach  260   237   1020    8401    84883

Java 11 Hotspot VM - 3,4GHz Intel Xeon, 8 GB, Windows 10 Pro
(mesin yang sama seperti di atas, versi JDK yang berbeda)

                            1    10    100    1000    10000
        iterator.forEach   20   104    940    8350    88918
                for:each   50   140    991    8497    89873
          for with index   37   140    945    8646    90402
 iterable.stream.forEach  200   270   1054    8558    87449

Java 11 OpenJ9 VM - 3.4GHz Intel Xeon, 8 GB, Windows 10 Pro
(mesin yang sama dan versi JDK seperti di atas, VM yang berbeda)

                            1    10    100    1000    10000
        iterator.forEach  211   475   3499   33631   336108
                for:each  200   375   2793   27249   272590
          for with index  384   467   2718   26036   261408
 iterable.stream.forEach  515   714   3096   26320   262786

Java 8 Hotspot VM - AMD 2.8GHz, 64 GB, Windows Server 2016

                            1    10    100    1000    10000
        iterator.forEach   95   192   2076   19269   198519
                for:each  157   224   2492   25466   248494
          for with index  140   368   2084   22294   207092
 iterable.stream.forEach  946   687   2206   21697   238457

Java 11 Hotspot VM - AMD 2.8GHz, 64 GB, Windows Server 2016
(mesin yang sama seperti di atas, versi JDK yang berbeda)

                            1    10    100    1000    10000
        iterator.forEach   72   269   1972   23157   229445
                for:each  192   376   2114   24389   233544
          for with index  165   424   2123   20853   220356
 iterable.stream.forEach  921   660   2194   23840   204817

Java 11 OpenJ9 VM - AMD 2.8GHz, 64 GB, Windows Server 2016
(versi mesin dan JDK yang sama seperti di atas, VM berbeda)

                            1    10    100    1000    10000
        iterator.forEach  592   914   7232   59062   529497
                for:each  477  1576  14706  129724  1190001
          for with index  893   838   7265   74045   842927
 iterable.stream.forEach 1359  1782  11869  104427   958584

Implementasi VM yang Anda pilih juga membuat perbedaan Hotspot / OpenJ9 / etc.


3
Itu jawaban yang sangat bagus, terima kasih! Tetapi dari pandangan pertama (dan juga dari pandangan kedua) tidak jelas metode apa yang sesuai dengan eksperimen apa.
torina

Saya merasa jawaban ini membutuhkan lebih banyak suara untuk tes kode :).
Cory

untuk contoh pengujian +1
Centos

8

Tidak ada perbedaan antara keduanya yang telah Anda sebutkan, setidaknya secara konseptual, Collection.forEach()itu hanya sebuah steno.

Secara internal stream()versi memiliki overhead yang agak lebih karena penciptaan objek, tetapi melihat waktu berjalan tidak ada overhead di sana.

Kedua implementasi berakhir iterasi atas collectionkonten satu kali, dan selama iterasi mencetak elemen.


Overhead penciptaan objek yang Anda sebutkan, apakah Anda merujuk pada Streamyang dibuat atau objek individu? AFAIK, a Streamtidak menduplikasi elemen.
Raffi Khatchadourian

30
Jawaban ini tampaknya bertentangan dengan jawaban bagus yang ditulis oleh pria yang mengembangkan perpustakaan inti Java di Oracle Corporation.
Dawood ibn Kareem

0

Collection.forEach () menggunakan iterator koleksi (jika ada yang ditentukan). Itu berarti bahwa urutan pemrosesan item didefinisikan. Sebaliknya, urutan pemrosesan Collection.stream (). ForEach () tidak ditentukan.

Dalam kebanyakan kasus, itu tidak membuat perbedaan mana dari dua yang kita pilih. Aliran paralel memungkinkan kita untuk mengeksekusi aliran di banyak utas, dan dalam situasi seperti itu, urutan eksekusi tidak ditentukan. Java hanya membutuhkan semua utas untuk menyelesaikan sebelum operasi terminal apa pun, seperti Collectors.toList (), dipanggil. Mari kita lihat contoh di mana pertama kita memanggil forach () langsung pada koleksi, dan kedua, pada aliran paralel:

list.forEach(System.out::print);
System.out.print(" ");
list.parallelStream().forEach(System.out::print);

Jika kita menjalankan kode beberapa kali, kita melihat bahwa list.forEach () memproses item dalam urutan penyisipan, sementara list.parallelStream (). ForEach () menghasilkan hasil yang berbeda pada setiap run. Satu kemungkinan keluaran adalah:

ABCD CDBA

Yang lain adalah:

ABCD DBCA
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.