Sự khác biệt giữa Collection.stream (). ForEach () và Collection.forEach () là gì?


286

Tôi hiểu rằng với .stream(), tôi có thể sử dụng các hoạt động chuỗi như .filter()hoặc sử dụng luồng song song. Nhưng sự khác biệt giữa chúng là gì nếu tôi cần thực hiện các thao tác nhỏ (ví dụ: in các thành phần của danh sách)?

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

Câu trả lời:


287

Đối với các trường hợp đơn giản như một minh họa, chúng hầu hết giống nhau. Tuy nhiên, có một số khác biệt tinh tế có thể là đáng kể.

Một vấn đề là với đặt hàng. Với Stream.forEach, thứ tự là không xác định . Tuy nhiên, điều đó không thể xảy ra với các luồng liên tiếp, tuy nhiên, nó nằm trong đặc tả Stream.forEachđể thực hiện theo một số thứ tự tùy ý. Điều này không xảy ra thường xuyên trong các luồng song song. Ngược lại, Iterable.forEachluôn luôn được thực hiện theo thứ tự lặp lại Iterable, nếu được chỉ định.

Một vấn đề khác là với tác dụng phụ. Hành động được chỉ định trong Stream.forEachđược yêu cầu là không can thiệp . (Xem tài liệu gói java.util.stream .) Có Iterable.forEachkhả năng có ít hạn chế hơn. Đối với các bộ sưu tập java.util, Iterable.forEachthường sẽ sử dụng bộ sưu tập đó Iterator, hầu hết các bộ sưu tập được thiết kế không nhanh và sẽ ném ConcurrentModificationExceptionnếu bộ sưu tập được sửa đổi cấu trúc trong quá trình lặp. Tuy nhiên, những sửa đổi không có cấu trúc được cho phép trong quá trình lặp. Ví dụ, tài liệu lớp ArrayList nói "chỉ thiết lập giá trị của một phần tử không phải là sửa đổi cấu trúc." Do đó, hành động choArrayList.forEachđược phép đặt giá trị bên dưới ArrayListmà không gặp vấn đề gì.

Các bộ sưu tập đồng thời là một lần nữa khác nhau. Thay vì thất bại nhanh, chúng được thiết kế để nhất quán yếu . Định nghĩa đầy đủ là ở liên kết đó. Ngắn gọn, mặc dù, xem xét ConcurrentLinkedDeque. Hành động được truyền cho forEachphương thức của nó được phép sửa đổi deque cơ bản, thậm chí về mặt cấu trúc và ConcurrentModificationExceptionkhông bao giờ được ném. Tuy nhiên, sửa đổi xảy ra có thể hoặc không thể nhìn thấy trong lần lặp này. (Do đó tính nhất quán "yếu".)

Vẫn còn một sự khác biệt nữa nếu Iterable.forEachlặp đi lặp lại trên một bộ sưu tập được đồng bộ hóa. Trên một bộ sưu tập như vậy, Iterable.forEach lấy khóa của bộ sưu tập một lần và giữ nó trên tất cả các lệnh gọi đến phương thức hành động. Cuộc Stream.forEachgọi sử dụng bộ chia của bộ sưu tập, không khóa và dựa trên quy tắc không can thiệp phổ biến. Bộ sưu tập sao lưu luồng có thể được sửa đổi trong quá trình lặp và nếu có, một ConcurrentModificationExceptionhành vi không nhất quán có thể dẫn đến.


Iterable.forEach takes the collection's lock. Thông tin này từ đâu? Tôi không thể tìm thấy hành vi như vậy trong các nguồn JDK.
turbanoff


@Stuart, bạn có thể giải thích về việc không can thiệp. Stream.forEach () cũng sẽ ném ConcurrencyModificationException (ít nhất là đối với tôi).
yuranos

1
@ yuranos87 Nhiều bộ sưu tập như ArrayListkiểm tra khá nghiêm ngặt để sửa đổi đồng thời, và do đó sẽ thường bị ném ConcurrentModificationException. Nhưng điều này không được đảm bảo, đặc biệt đối với các luồng song song. Thay vì CME, bạn có thể nhận được một câu trả lời bất ngờ. Cũng xem xét sửa đổi phi cấu trúc đối với nguồn phát. Đối với các luồng song song, bạn không biết luồng nào sẽ xử lý một phần tử cụ thể, cũng như liệu nó có được xử lý tại thời điểm nó được sửa đổi hay không. Điều này thiết lập một điều kiện cuộc đua, nơi bạn có thể nhận được các kết quả khác nhau trên mỗi lần chạy và không bao giờ có được CME.
Stuart Marks

30

Câu trả lời này liên quan đến chính nó với việc thực hiện các vòng lặp khác nhau. Nó chỉ phù hợp với các vòng lặp được gọi là RẤT OFTEN (như hàng triệu cuộc gọi). Trong hầu hết các trường hợp, nội dung của vòng lặp sẽ là yếu tố đắt nhất. Đối với các tình huống mà bạn lặp lại thực sự thường xuyên, điều này vẫn có thể được quan tâm.

Bạn nên lặp lại thử nghiệm này trong hệ thống đích vì đây là cách triển khai cụ thể, ( mã nguồn đầy đủ ).

Tôi chạy openjdk phiên bản 1.8.0_111 trên máy Linux nhanh.

Tôi đã viết một bài kiểm tra lặp lại 10 ^ 6 lần trong Danh sách bằng mã này với các kích cỡ khác nhau cho integers(10 ^ 0 -> 10 ^ 5 mục).

Kết quả là bên dưới, phương pháp nhanh nhất khác nhau tùy thuộc vào số lượng mục trong danh sách.

Nhưng vẫn trong tình huống xấu nhất, lặp lại hơn 10 ^ 5 mục 10 ^ 6 lần mất 100 giây cho người thực hiện tồi nhất, vì vậy những cân nhắc khác quan trọng hơn trong hầu hết tất cả các tình huống.

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

Dưới đây là thời gian của tôi: mili giây / chức năng / số lượng mục trong danh sách. Mỗi lần chạy là 10 ^ 6 vòng.

                           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

Nếu bạn lặp lại thử nghiệm, tôi đã đăng mã nguồn đầy đủ . Vui lòng chỉnh sửa câu trả lời này và thêm cho bạn kết quả bằng ký hiệu của hệ thống được kiểm tra.


Sử dụng MacBook Pro, Intel Core i7 2,5 GHz, 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

VM 8 Hotspot VM - Intel Xeon 3,4 GHz, 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

VM 11 Hotspot VM - Intel Xeon 3,4 GHz, 8 GB, Windows 10 Pro
(cùng máy như trên, phiên bản JDK khác nhau)

                            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 - Intel Xeon 3,4 GHz, 8 GB, Windows 10 Pro
(cùng máy và phiên bản JDK như trên, VM khác nhau)

                            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

VM 8 Hotspot VM - AMD 2,8 GHz, 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,8 GHz, 64 GB, Windows Server 2016
(cùng một máy như trên, phiên bản JDK khác nhau)

                            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 - 2.8GHz AMD, 64 GB, Windows Server 2016
(cùng máy và phiên bản JDK như trên, VM khác nhau)

                            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

Việc triển khai VM bạn chọn cũng tạo ra sự khác biệt Hotspot / OpenJ9 / etc.


3
Đó là một câu trả lời rất hay, cảm ơn! Nhưng từ cái nhìn đầu tiên (và cũng từ cái thứ hai) không rõ phương pháp nào tương ứng với thí nghiệm nào.
torina

Tôi cảm thấy câu trả lời này cần nhiều phiếu hơn cho bài kiểm tra mã :).
Cory

cho các ví dụ kiểm tra +1
Centos

8

Không có sự khác biệt giữa hai bạn đã đề cập, ít nhất về mặt khái niệm, Collection.forEach()chỉ là một tốc ký.

Bên trong stream()phiên bản có phần chi phí cao hơn do tạo đối tượng, nhưng nhìn vào thời gian hoạt động, nó không có chi phí hoạt động ở đó.

Cả hai lần thực hiện đều lặp lại collectionnội dung một lần và trong quá trình lặp lại in ra phần tử.


Chi phí tạo đối tượng mà bạn đề cập, bạn đang đề cập đến việc Streamđược tạo hay các đối tượng riêng lẻ? AFAIK, a Streamkhông trùng lặp các yếu tố.
Raffi Khatchadourian 7/03/2015

30
Câu trả lời này dường như mâu thuẫn với câu trả lời xuất sắc được viết bởi người đàn ông phát triển các thư viện lõi Java tại Tập đoàn Oracle.
Dawood ibn Kareem

0

Collection.forEach () sử dụng trình vòng lặp của bộ sưu tập (nếu được chỉ định). Điều đó có nghĩa là thứ tự xử lý của các mục được xác định. Ngược lại, thứ tự xử lý của Collection.stream (). ForEach () không được xác định.

Trong hầu hết các trường hợp, nó không tạo ra sự khác biệt trong số hai chúng tôi chọn. Các luồng song song cho phép chúng ta thực hiện luồng trong nhiều luồng và trong các tình huống như vậy, thứ tự thực hiện không được xác định. Java chỉ yêu cầu tất cả các luồng kết thúc trước khi bất kỳ hoạt động đầu cuối nào, chẳng hạn như Collector.toList (), được gọi. Hãy xem xét một ví dụ nơi đầu tiên chúng ta gọi forEach () trực tiếp trên bộ sưu tập và thứ hai, trên một luồng song song:

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

Nếu chúng tôi chạy mã nhiều lần, chúng tôi sẽ thấy list.forEach () xử lý các mục theo thứ tự chèn, trong khi list.poolStream (). ForEach () tạo ra kết quả khác nhau ở mỗi lần chạy. Một đầu ra có thể là:

ABCD CDBA

Một số khác là:

ABCD DBCA
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.