Collection.stream (). forEach () และ Collection.forEach () แตกต่างกันอย่างไร?


286

ฉันเข้าใจว่าด้วย.stream()ฉันสามารถใช้การปฏิบัติการลูกโซ่เช่น.filter()หรือใช้กระแสข้อมูลแบบขนาน แต่อะไรคือความแตกต่างระหว่างพวกเขาหากฉันต้องการดำเนินการเล็ก ๆ (เช่นการพิมพ์องค์ประกอบของรายการ)

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

คำตอบ:


287

สำหรับกรณีที่เรียบง่ายเช่นภาพที่แสดงพวกเขาส่วนใหญ่เหมือนกัน อย่างไรก็ตามมีความแตกต่างเล็กน้อยที่อาจมีนัยสำคัญ

ปัญหาหนึ่งคือการสั่งซื้อ ด้วยStream.forEachคำสั่งซื้อที่มีการกำหนด มันไม่น่าจะเกิดขึ้นกับลำธารเรียงตามลำดับ แต่ก็อยู่ในข้อกำหนดสำหรับStream.forEachการดำเนินการตามลำดับโดยพลการ สิ่งนี้เกิดขึ้นบ่อยครั้งในสตรีมขนาน ในทางตรงกันข้ามIterable.forEachจะถูกดำเนินการตามลำดับการวนซ้ำของIterableหากมีการระบุไว้

ปัญหาก็คือมีผลข้างเคียง การกระทำที่ระบุไว้ในStream.forEachจะต้องไม่ใช่การแทรกแซง (โปรดดูเอกสารแพคเกจ java.util.stream ) Iterable.forEachอาจมีข้อ จำกัด น้อยลง สำหรับคอลเลคชั่java.utilIterable.forEachโดยทั่วไปจะใช้คอลเลกชันนั้นIteratorซึ่งส่วนใหญ่ได้รับการออกแบบให้ล้มเหลวอย่างรวดเร็วและจะโยนทิ้งConcurrentModificationExceptionหากคอลเลกชันนั้นมีการปรับเปลี่ยนโครงสร้างในระหว่างการทำซ้ำ อย่างไรก็ตามการดัดแปลงที่ไม่ได้มีโครงสร้างได้รับอนุญาตในระหว่างการทำซ้ำ ตัวอย่างเช่นเอกสารประกอบคลาส ArrayListกล่าวว่า "เพียงแค่การตั้งค่าขององค์ประกอบไม่ใช่การดัดแปลงโครงสร้าง" ดังนั้นการกระทำของArrayList.forEachได้รับอนุญาตให้ตั้งค่าในพื้นฐานArrayListโดยไม่มีปัญหา

คอลเล็กชันที่เกิดขึ้นพร้อมกันนั้นแตกต่างกันอีกครั้ง แทนที่จะล้มเหลวอย่างรวดเร็วพวกเขาถูกออกแบบมาให้สอดคล้องกันอย่างอ่อน คำจำกัดความเต็มอยู่ที่ลิงค์นั้น สั้น ๆ ConcurrentLinkedDequeแต่พิจารณา การกระทำที่ผ่านมาของforEachวิธีการที่จะได้รับอนุญาตให้ปรับเปลี่ยน deque พื้นฐานแม้โครงสร้างและConcurrentModificationExceptionไม่เคยโยน อย่างไรก็ตามการปรับเปลี่ยนที่เกิดขึ้นอาจหรือไม่อาจมองเห็นได้ในการทำซ้ำนี้ (ดังนั้นความสอดคล้อง "อ่อนแอ")

ยังคงเห็นความแตกต่างอื่นได้หากIterable.forEachทำซ้ำในคอลเล็กชันที่ซิงโครไนซ์ ในคอลเล็กชันดังกล่าวให้Iterable.forEach ใช้การล็อกของคอลเล็กชันหนึ่งครั้งและเก็บไว้ในการโทรทั้งหมดไปยังวิธีการดำเนินการ การStream.forEachเรียกใช้ตัวแยกสัญญาณของคอลเลกชันซึ่งไม่ล็อคและขึ้นอยู่กับกฎทั่วไปของการไม่แทรกแซง คอลเลกชันที่สำรองข้อมูลสตรีมสามารถแก้ไขได้ในระหว่างการทำซ้ำและหากเป็นเช่นนั้นConcurrentModificationExceptionพฤติกรรมที่ไม่แน่นอนอาจส่งผล


Iterable.forEach takes the collection's lock. ข้อมูลนี้มาจากไหน? ฉันไม่พบพฤติกรรมดังกล่าวในแหล่งข้อมูล JDK
turbanoff


@ Stuart คุณสามารถอธิบายรายละเอียดเกี่ยวกับการไม่รบกวน Stream.forEach () จะโยน ConcurrentModificationException (อย่างน้อยสำหรับฉัน)
yuranos

1
@ yuranos87 คอลเลกชันหลายอย่างเช่นมีการตรวจสอบอย่างเข้มงวดเป็นธรรมสำหรับการปรับเปลี่ยนพร้อมกันและด้วยเหตุนี้มักจะโยนArrayList ConcurrentModificationExceptionแต่สิ่งนี้ไม่ได้รับประกันโดยเฉพาะสำหรับสตรีมขนาน แทนที่จะเป็น CME คุณอาจได้รับคำตอบที่ไม่คาดคิด พิจารณาการดัดแปลงโครงสร้างแบบไม่มีโครงสร้างให้กับแหล่งข้อมูลสตรีม สำหรับสตรีมแบบขนานคุณไม่ทราบว่าเธรดจะประมวลผลองค์ประกอบเฉพาะหรือไม่ว่าจะถูกประมวลผลในเวลาที่แก้ไขหรือไม่ นี่เป็นการตั้งค่าสภาพการแข่งขันซึ่งคุณอาจได้รับผลลัพธ์ที่แตกต่างกันในการวิ่งแต่ละครั้งและไม่เคยได้รับ CME
Stuart Marks

30

คำตอบนี้เกี่ยวข้องกับประสิทธิภาพของการนำลูปไปใช้งานต่าง ๆ มันเกี่ยวข้องเพียงเล็กน้อยสำหรับลูปที่เรียกว่า VERY OFTEN (เช่นการโทรนับล้านครั้ง) ในกรณีส่วนใหญ่เนื้อหาของลูปจะเป็นองค์ประกอบที่มีราคาแพงที่สุด สำหรับสถานการณ์ที่คุณวนซ้ำบ่อยครั้งสิ่งนี้อาจเป็นที่สนใจ

คุณควรทำซ้ำการทดสอบนี้ภายใต้ระบบเป้าหมายเพราะนี่คือการใช้งานเฉพาะ ( รหัสที่มาแบบเต็ม )

ฉันใช้ openjdk รุ่น 1.8.0_111 บนเครื่องลีนุกซ์ที่เร็ว

ฉันเขียนการทดสอบที่วนรอบ 10 ^ 6 ครั้งในรายการโดยใช้รหัสนี้ด้วยขนาดที่แตกต่างกันสำหรับintegers(10 ^ 0 -> 10 ^ 5 รายการ)

ผลลัพธ์อยู่ด้านล่างวิธีที่เร็วที่สุดจะแตกต่างกันไปขึ้นอยู่กับปริมาณของรายการในรายการ

แต่ยังอยู่ในสถานการณ์ที่เลวร้ายที่สุดการวนซ้ำ 10 ^ 5 รายการ 10 ^ 6 ครั้งใช้เวลา 100 วินาทีสำหรับนักแสดงที่แย่ที่สุดดังนั้นข้อควรพิจารณาอื่น ๆ จึงมีความสำคัญมากกว่าในทุกสถานการณ์

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

นี่คือการกำหนดเวลาของฉัน: มิลลิวินาที / ฟังก์ชั่น / จำนวนรายการในรายการ การวิ่งแต่ละครั้งคือ 10 ^ 6 ลูป

                           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

ถ้าคุณทำซ้ำการทดลองที่ผมโพสต์รหัสที่มาเต็ม โปรดแก้ไขคำตอบนี้และเพิ่มผลลัพธ์ด้วยเครื่องหมายของระบบที่ผ่านการทดสอบ


ใช้ 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
(เครื่องเดียวกันกับรุ่น JDK ที่ต่างกัน)

                            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
(เครื่องเดียวกันและรุ่น JDK ข้างต้น, VM ที่แตกต่างกัน)

                            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 - 2.8GHz AMD, 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 - 2.8GHz AMD, 64 GB, Windows Server 2016
(เครื่องเดียวกันข้างต้น, เวอร์ชั่น JDK ที่แตกต่างกัน)

                            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
(เครื่องเดียวกันและรุ่น JDK ข้างต้น, VM ที่แตกต่างกัน)

                            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

การติดตั้ง VM ที่คุณเลือกสร้างความแตกต่าง Hotspot / OpenJ9 / etc


3
นั่นเป็นคำตอบที่ดีมากขอบคุณ! แต่จากภาพรวมครั้งแรก (และจากภาพที่สอง) มันไม่ชัดเจนว่าวิธีการใดที่สอดคล้องกับการทดสอบแบบใด
torina

ฉันรู้สึกว่าคำตอบนี้ต้องการคะแนนโหวตมากขึ้นสำหรับการทดสอบโค้ด :)
คอรี

สำหรับตัวอย่างการทดสอบ +1
Centos

8

ไม่มีความแตกต่างระหว่างสองสิ่งที่คุณพูดถึง แต่อย่างน้อยที่สุดแนวคิดCollection.forEach()ก็เป็นเพียงการจดชวเลข

ภายในstream()รุ่นมีค่าใช้จ่ายค่อนข้างมากเนื่องจากการสร้างวัตถุ แต่ดูที่เวลาทำงานมันไม่มีค่าใช้จ่ายที่นั่น

การใช้งานทั้งสองสิ้นสุดลงซ้ำcollectionเนื้อหามากกว่าหนึ่งครั้งและในระหว่างการทำซ้ำพิมพ์องค์ประกอบ


ค่าใช้จ่ายในการสร้างวัตถุที่คุณพูดถึงคุณหมายถึงสิ่งที่Streamถูกสร้างหรือวัตถุแต่ละชิ้นหรือไม่? AFAIK, a Streamไม่ได้ทำซ้ำองค์ประกอบ
Raffi Khatchadourian

30
คำตอบนี้ดูเหมือนจะขัดแย้งกับคำตอบที่ยอดเยี่ยมที่เขียนโดยสุภาพบุรุษผู้พัฒนาห้องสมุดแกน Java ที่ Oracle Corporation
Dawood ibn Kareem

0

Collection.forEach () ใช้ตัววนซ้ำของคอลเลกชัน (หากระบุไว้) นั่นหมายความว่ามีการกำหนดลำดับการประมวลผลของรายการ ในทางตรงกันข้ามลำดับการประมวลผลของ Collection.stream (). forEach () ไม่ได้ถูกกำหนดไว้

ในกรณีส่วนใหญ่มันไม่ได้สร้างความแตกต่างซึ่งทั้งสองอย่างที่เราเลือก สตรีมแบบขนานช่วยให้เราสามารถดำเนินการสตรีมในหลายเธรดและในสถานการณ์เช่นนี้คำสั่งการดำเนินการจะไม่ได้กำหนด Java ต้องการเฉพาะเธรดทั้งหมดให้เสร็จสิ้นก่อนการดำเนินการเทอร์มินัลเช่น Collector.toList () จะถูกเรียกใช้ ลองดูตัวอย่างที่เราเรียก forEach () โดยตรงบนคอลเล็กชันและอันดับที่สองในสตรีมแบบขนาน:

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

หากเราเรียกใช้รหัสหลาย ๆ ครั้งเราจะเห็นว่า list.forEach () ประมวลผลรายการตามลำดับการแทรกในขณะที่ list.parallelStream (). forEach () สร้างผลลัพธ์ที่แตกต่างกันในการทำงานแต่ละครั้ง หนึ่งผลลัพธ์ที่เป็นไปได้คือ:

ABCD CDBA

อีกหนึ่งคือ:

ABCD DBCA
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.