Perbedaan Antara Collection.stream (). ForEach () dan Collection.forEach ()

1. Perkenalan

Ada beberapa opsi untuk mengulang koleksi di Java. Dalam tutorial singkat ini, kita akan melihat dua pendekatan yang mirip - Collection.stream (). ForEach () dan Collection.forEach () .

Dalam kebanyakan kasus, keduanya akan memberikan hasil yang sama, namun, ada beberapa perbedaan kecil yang akan kita lihat.

2. Ikhtisar

Pertama, mari buat daftar untuk mengulang:

List list = Arrays.asList("A", "B", "C", "D");

Cara paling mudah adalah menggunakan for-loop yang disempurnakan:

for(String s : list) { //do something with s } 

Jika kita ingin menggunakan Java gaya fungsional, kita juga bisa menggunakan forEach () . Kami dapat melakukannya langsung di koleksi:

Consumer consumer = s -> { System.out::println }; list.forEach(consumer); 

Atau, kita bisa memanggil forEach () di aliran koleksi:

list.stream().forEach(consumer); 

Kedua versi akan mengulangi daftar dan mencetak semua elemen:

ABCD ABCD

Dalam kasus sederhana ini, tidak ada bedanya forEach () yang kita gunakan.

3. Perintah Eksekusi

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

Dalam kebanyakan kasus, tidak ada bedanya yang mana dari keduanya yang kita pilih.

3.1. Aliran Paralel

Aliran paralel memungkinkan kita untuk menjalankan aliran di beberapa utas, dan dalam situasi seperti itu, urutan eksekusi tidak ditentukan. Java hanya membutuhkan semua utas untuk diselesaikan sebelum operasi terminal apa pun, seperti Collectors.toList () , dipanggil.

Mari kita lihat contoh di mana kita pertama kali memanggil forEach () 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 list.forEach () memproses item dalam urutan penyisipan, sementara list.parallelStream (). ForEach () menghasilkan hasil yang berbeda pada setiap proses.

Salah satu kemungkinan keluarannya adalah:

ABCD CDBA

Yang lainnya adalah:

ABCD DBCA

3.2. Iterator Kustom

Mari tentukan daftar dengan iterator khusus untuk mengulang koleksi dalam urutan terbalik:

class ReverseList extends ArrayList { @Override public Iterator iterator() { int startIndex = this.size() - 1; List list = this; Iterator it = new Iterator() { private int currentIndex = startIndex; @Override public boolean hasNext() { return currentIndex >= 0; } @Override public String next() { String next = list.get(currentIndex); currentIndex--; return next; } @Override public void remove() { throw new UnsupportedOperationException(); } }; return it; } } 

Saat kami mengulang daftar, sekali lagi dengan forEach () langsung di koleksi lalu di aliran:

List myList = new ReverseList(); myList.addAll(list); myList.forEach(System.out::print); System.out.print(" "); myList.stream().forEach(System.out::print); 

Kami mendapatkan hasil yang berbeda:

DCBA ABCD 

Alasan untuk hasil yang berbeda adalah karena forEach () yang digunakan secara langsung pada daftar menggunakan iterator khusus, sementara stream (). ForEach () hanya mengambil elemen satu per satu dari daftar, mengabaikan iterator.

4. Modifikasi Koleksi

Banyak koleksi (misalnya, ArrayList atau HashSet ) tidak boleh dimodifikasi secara struktural saat mengulanginya. Jika sebuah elemen dihapus atau ditambahkan selama iterasi, kita akan mendapatkan pengecualian ConcurrentModification .

Lebih jauh, koleksi dirancang untuk gagal-cepat, yang berarti pengecualian dilemparkan segera setelah ada modifikasi.

Demikian pula, kita akan mendapatkan pengecualian ConcurrentModification saat kita menambah atau menghapus elemen selama eksekusi pipeline streaming. Namun, pengecualian akan dibuang nanti.

Perbedaan halus lainnya antara dua metode forEach () adalah bahwa Java secara eksplisit mengizinkan modifikasi elemen menggunakan iterator. Aliran, sebaliknya, harus tidak mengganggu.

Mari kita lihat menghapus dan memodifikasi elemen lebih detail.

4.1. Menghapus sebuah Elemen

Mari kita definisikan operasi yang menghapus elemen terakhir ("D") dari daftar kita:

Consumer removeElement = s -> { System.out.println(s + " " + list.size()); if (s != null && s.equals("A")) { list.remove("D"); } };

Ketika kita mengulang daftar, elemen terakhir dihapus setelah elemen pertama ("A") dicetak:

list.forEach(removeElement);

Karena forEach () gagal-cepat, kami menghentikan iterasi dan melihat pengecualian sebelum elemen berikutnya diproses:

A 4 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList.forEach(ArrayList.java:1252) at ReverseList.main(ReverseList.java:1)

Let's see what happens if we use stream().forEach() instead:

list.stream().forEach(removeElement);

Here, we continue iterating over the whole list before we see an exception:

A 4 B 3 C 3 null 3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at ReverseList.main(ReverseList.java:1)

However, Java does not guarantee that a ConcurrentModificationException is thrown at all. That means we should never write a program that depends on this exception.

4.2. Changing Elements

We can change an element while iterating over a list:

list.forEach(e -> { list.set(3, "E"); });

However, while there is no problem with doing this using either Collection.forEach() or stream().forEach(), Java requires an operation on a stream to be non-interfering. This means that elements shouldn't be modified during the execution of the stream pipeline.

The reason behind this is that the stream should facilitate parallel execution. Here, modifying elements of a stream could lead to unexpected behavior.

5. Conclusion

In this article, we saw some examples that show the subtle differences between Collection.forEach() and Collection.stream().forEach().

However, it's important to note that all the examples shown above are trivial and are merely meant to compare the two ways of iterating over a collection. We shouldn't write code whose correctness relies on the shown behavior.

If we don't require a stream but only want to iterate over a collection, the first choice should be using forEach() directly on the collection.

Kode sumber untuk contoh dalam artikel ini tersedia di GitHub.