Perbezaan Antara Koleksi.stream (). ForEach () dan Collection.forEach ()

1. Pengenalan

Ada beberapa pilihan untuk mengulang-ulang koleksi di Java. Dalam tutorial ringkas ini, kita akan melihat dua pendekatan yang serupa - Collection.stream (). ForEach () dan Collection.forEach () .

Dalam kebanyakan kes, kedua-duanya akan memberikan hasil yang sama, namun, ada beberapa perbezaan halus yang akan kita lihat.

2. Gambaran keseluruhan

Pertama, mari buat senarai untuk berulang:

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

Cara paling mudah adalah menggunakan gelung untuk yang dipertingkatkan:

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

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

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

Atau, kita boleh memanggilEach () pada aliran koleksi:

list.stream().forEach(consumer); 

Kedua-dua versi akan berulang pada senarai dan mencetak semua elemen:

ABCD ABCD

Dalam kes mudah ini, tidak ada perbezaan yang untuk setiap () yang kita gunakan.

3. Perintah Pelaksanaan

Collection.forEach () menggunakan iterator koleksi (jika ditentukan). Ini bermaksud bahawa pesanan pemprosesan item ditentukan. Sebaliknya, susunan pemprosesan Collection.stream (). ForEach () tidak ditentukan.

Dalam kebanyakan kes, tidak ada perbezaan dari mana yang kita pilih.

3.1. Aliran Selari

Aliran selari membolehkan kita melaksanakan aliran dalam beberapa utas, dan dalam situasi seperti itu, perintah pelaksanaan tidak ditentukan. Java hanya memerlukan semua utas selesai sebelum operasi terminal, seperti Collectors.toList () , dipanggil.

Mari kita lihat contoh di mana pertama kali kita memanggilEach () secara langsung pada koleksi, dan kedua, pada aliran selari:

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

Sekiranya kita menjalankan kod beberapa kali, kita melihat bahawa list.forEach () memproses item mengikut urutan penyisipan, sementara list.parallelStream (). ForEach () menghasilkan hasil yang berbeza pada setiap larian.

Satu kemungkinan output adalah:

ABCD CDBA

Yang lain ialah:

ABCD DBCA

3.2. Iterator Tersuai

Mari tentukan senarai dengan iterator khusus untuk mengulangi 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; } } 

Apabila kita mengulangi senarai, sekali lagi dengan forEach () secara langsung pada koleksi dan kemudian pada aliran:

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

Kami mendapat hasil yang berbeza:

DCBA ABCD 

Sebab untuk hasil yang berbeza adalah kerana forEach () yang digunakan secara langsung dalam senarai menggunakan iterator tersuai, sementara stream (). ForEach () hanya mengambil elemen satu per satu dari senarai, mengabaikan iterator.

4. Pengubahsuaian Koleksi

Banyak koleksi (contohnya, ArrayList atau HashSet ) tidak boleh diubah secara struktural semasa melakukan lelaran ke atasnya. Sekiranya elemen dikeluarkan atau ditambahkan semasa lelaran, kami akan mendapat pengecualian ConcurrentModification .

Selanjutnya, koleksi dirancang untuk cepat gagal, yang bermaksud bahawa pengecualian dilemparkan sebaik sahaja ada pengubahsuaian.

Begitu juga, kita akan mendapat pengecualian ConcurrentModification ketika kita menambah atau membuang elemen semasa pelaksanaan aliran paip. Walau bagaimanapun, pengecualian akan dilemparkan kemudian.

Perbezaan halus antara kedua kaedah forEach () adalah bahawa Java secara eksplisit membenarkan elemen pengubahsuaian menggunakan iterator. Sebaliknya, aliran tidak boleh mengganggu.

Mari lihat penghapusan dan pengubahsuaian elemen dengan lebih terperinci.

4.1. Mengeluarkan Elemen

Mari tentukan operasi yang membuang elemen terakhir ("D") senarai kami:

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

Apabila kita mengulangi senarai, elemen terakhir dikeluarkan setelah elemen pertama ("A") dicetak:

list.forEach(removeElement);

Oleh kerana forEach () gagal cepat, kami berhenti mengulangi dan melihat pengecualian sebelum elemen seterusnya 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.

Kod sumber untuk contoh dalam artikel ini terdapat di GitHub.