วิธีเพิ่มองค์ประกอบของสตรีม Java8 ลงในรายการที่มีอยู่


155

Javadoc of Collectorแสดงวิธีรวบรวมองค์ประกอบของสตรีมในรายการใหม่ มีหนึ่งซับที่เพิ่มผลลัพธ์ลงใน ArrayList ที่มีอยู่หรือไม่


1
มีคำตอบอยู่ที่นี่แล้ว มองหารายการ“ เพิ่มไปยังที่มีอยู่Collection
Holger

คำตอบ:


198

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

คำตอบสั้น ๆ คือไม่อย่างน้อยไม่ใช่โดยทั่วไปคุณไม่ควรใช้ a Collectorเพื่อแก้ไขคอลเล็กชันที่มีอยู่

เหตุผลก็คือนักสะสมได้รับการออกแบบมาเพื่อรองรับความเท่าเทียมแม้ในคอลเล็กชันที่ไม่ปลอดภัยสำหรับเธรด วิธีที่พวกเขาทำเช่นนี้คือให้แต่ละเธรดทำงานอย่างอิสระในการรวบรวมผลลัพธ์ระดับกลางของตัวเอง วิธีที่แต่ละเธรดได้รับคอลเล็กชันของตัวเองคือเรียกสิ่งCollector.supplier()ที่ต้องการเพื่อส่งคืนคอลเล็กชันใหม่ในแต่ละครั้ง

การรวมผลลัพธ์ระดับกลางเหล่านี้จะถูกรวมเข้าด้วยกันอีกครั้งในรูปแบบการ จำกัด เธรดจนกว่าจะมีการรวบรวมผลลัพธ์เดียว นี่คือผลลัพธ์สุดท้ายของการcollect()ดำเนินการ

คำตอบสองสามคำจากBalderและassyliasแนะนำให้ใช้Collectors.toCollection()แล้วส่งผู้จำหน่ายที่ส่งคืนรายการที่มีอยู่แทนที่จะเป็นรายการใหม่ สิ่งนี้ละเมิดข้อกำหนดของซัพพลายเออร์ซึ่งส่งคืนคอลเล็กชันใหม่ที่ว่างเปล่าในแต่ละครั้ง

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

ลองมาตัวอย่างง่าย ๆ :

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

ArrayIndexOutOfBoundsExceptionเมื่อฉันเรียกใช้โปรแกรมนี้ผมมักจะได้รับการ นี่เป็นเพราะหลายเธรดกำลังทำงานอยู่ArrayListโครงสร้างข้อมูลที่ไม่ปลอดภัยของเธรด ตกลงมาทำให้มันซิงโครไนซ์กัน:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

สิ่งนี้จะไม่ล้มเหลวอีกต่อไปโดยมีข้อยกเว้น แต่แทนที่จะเป็นผลลัพธ์ที่คาดหวัง:

[foo, 0, 1, 2, 3]

มันให้ผลลัพธ์แปลก ๆ เช่นนี้:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

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

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

คุณอาจพูดว่า "ฉันจะตรวจสอบให้แน่ใจว่าได้สตรีมตามลำดับ" และไปข้างหน้าและเขียนโค้ดแบบนี้

stream.collect(Collectors.toCollection(() -> existingList))

อย่างไรก็ตาม. ฉันไม่แนะนำให้ทำเช่นนี้ หากคุณควบคุมสตรีมแน่นอนคุณสามารถรับประกันได้ว่าจะไม่ทำงานแบบขนาน ฉันคาดหวังว่ารูปแบบของการเขียนโปรแกรมจะปรากฏขึ้นในที่ที่มีการส่งกระแสข้อมูลแทนคอลเลกชัน หากมีคนส่งกระแสข้อมูลให้คุณและคุณใช้รหัสนี้มันจะล้มเหลวหากกระแสข้อมูลขนานกัน บางคนอาจส่งสตรีมแบบต่อเนื่องให้คุณและรหัสนี้จะทำงานได้ดีในขณะที่ผ่านการทดสอบทั้งหมด ฯลฯ จากนั้นบางเวลาตามอำเภอใจในภายหลังรหัสอื่น ๆ ในระบบอาจเปลี่ยนเป็นการใช้สตรีมแบบขนานซึ่งจะทำให้โค้ดของคุณที่จะทำลาย

ตกลงจากนั้นอย่าลืมโทรเข้าsequential()สตรีมใด ๆ ก่อนใช้รหัสนี้:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

แน่นอนคุณจำได้ว่าต้องทำเช่นนี้ทุกครั้งใช่ไหม :-) สมมติว่าคุณทำ จากนั้นทีมงานจะสงสัยว่าทำไมการใช้งานแบบขนานอย่างรอบคอบทั้งหมดของพวกเขาไม่ได้ให้การเร่งความเร็วใด ๆ และอีกครั้งพวกเขาจะติดตามไปที่รหัสของคุณซึ่งบังคับให้สตรีมทั้งหมดทำงานตามลำดับ

อย่าทำมัน


คำอธิบายที่ดี! - ขอบคุณสำหรับการชี้แจงนี้ ฉันจะแก้ไขคำตอบของฉันเพื่อไม่แนะนำให้ทำเช่นนี้ด้วยสตรีมขนานที่เป็นไปได้
Balder

3
ถ้าคำถามคือถ้ามีหนึ่งซับเพื่อเพิ่มองค์ประกอบของกระแสเป็นรายการที่มีอยู่แล้วตอบสั้น ๆ คือใช่ ดูคำตอบของฉัน อย่างไรก็ตามฉันเห็นด้วยกับคุณว่าการใช้Collector.toCollection ()ร่วมกับรายการที่มีอยู่เป็นวิธีที่ผิด
nosid

จริง ฉันเดาว่าพวกเราที่เหลือล้วน แต่คิดถึงนักสะสม
Stuart Marks

คำตอบที่ดี! ฉันถูกล่อลวงให้ใช้วิธีแก้ปัญหาตามลำดับแม้ว่าคุณจะได้รับคำแนะนำอย่างชัดเจนเพราะตามที่ระบุไว้มันจะต้องทำงานได้ดี แต่ความจริงที่ว่า javadoc ต้องการให้อาร์กิวเมนต์ของซัพพลายเออร์ของtoCollectionวิธีการควรกลับคอลเลกชันใหม่และว่างเปล่าในแต่ละครั้งที่โน้มน้าวให้ฉันไม่ ฉันต้องการทำลายสัญญา javadoc ของคลาส Java หลัก
ซูม

1
@AlexCurvers forEachOrderedหากคุณต้องการกระแสที่จะมีผลข้างเคียงที่คุณเกือบจะแน่นอนต้องการที่จะใช้ ผลข้างเคียงรวมถึงการเพิ่มองค์ประกอบให้กับคอลเลกชันที่มีอยู่ไม่ว่าจะมีองค์ประกอบอยู่แล้วก็ตาม หากคุณต้องการองค์ประกอบกระแสของวางลงใหม่คอลเลกชันการใช้งานcollect(Collectors.toList())หรือหรือtoSet() toCollection()
Stuart Marks

169

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

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

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

โปรดทราบว่าวิธีนี้ใช้ได้กับทั้งลำาดับต่อเนื่องและลำาดับขนาน อย่างไรก็ตามมันไม่ได้รับประโยชน์จากการเห็นพ้องด้วย การอ้างอิงวิธีการที่ส่งไปยังforEachOrderedจะถูกดำเนินการตามลำดับเสมอ


6
+1 มันตลกดีที่มีคนจำนวนมากอ้างว่าไม่มีทางเป็นไปได้เมื่อมี Btw ผมรวมforEach(existing::add)ความเป็นไปได้ในการอื่นคำตอบสองเดือนที่ผ่านมา ฉันควรจะเพิ่มforEachOrderedเช่นกัน ...
โฮลเจอร์

5
มีเหตุผลใดบ้างที่คุณใช้forEachOrderedแทนforEach?
membersound

6
@membersound: forEachOrderedทำงานให้กับทั้งสองตามลำดับและขนานลำธาร ในทางตรงกันข้ามforEachอาจจะดำเนินการวัตถุฟังก์ชั่นพร้อมกันผ่านลำธารขนาน Vector<Integer>ในกรณีนี้วัตถุฟังก์ชั่นที่จะต้องตรงกันอย่างถูกต้องเช่นโดยการใช้
nosid

@BrianGoetz: ฉันต้องยอมรับว่าเอกสารของStream.forEachOrderedนั้นไม่แน่ชัด อย่างไรก็ตามฉันไม่เห็นการตีความที่สมเหตุสมผลของข้อกำหนดนี้ซึ่งไม่มีความสัมพันธ์เกิดขึ้นก่อนระหว่างการเรียกสองtarget::addครั้ง โดยไม่คำนึงถึงจากการที่หัวข้อวิธีการที่จะเรียกไม่มีการแข่งขันข้อมูล ฉันคาดหวังให้คุณรู้ว่า
nosid

นี่เป็นคำตอบที่มีประโยชน์ที่สุดเท่าที่ฉันกังวล มันแสดงให้เห็นถึงวิธีการที่ใช้งานได้จริงในการแทรกรายการลงในรายการที่มีอยู่จากสตรีมซึ่งเป็นคำถามที่ถาม (แม้จะมีคำว่า "รวบรวม" ที่ทำให้เข้าใจผิด)
Wheezil

12

คำตอบสั้น ๆคือไม่ (หรือควรจะไม่ใช่) แก้ไข:ใช่เป็นไปได้ (ดูคำตอบของ assylias ด้านล่าง) แต่อ่านต่อไป แก้ไข 2:แต่ดูคำตอบของ Stuart Marks ด้วยเหตุผลอื่นว่าทำไมคุณถึงยังไม่ควรทำ!

คำตอบอีกต่อไป:

วัตถุประสงค์ของโครงสร้างเหล่านี้ใน Java 8 คือการแนะนำแนวคิดบางประการของFunction Programmingให้กับภาษา ในฟังก์ชั่นการเขียนโปรแกรมโครงสร้างข้อมูลมักจะไม่ได้รับการแก้ไข แต่ใหม่จะถูกสร้างขึ้นมาจากสิ่งเก่าโดยใช้วิธีการแปลงเช่นแผนที่ตัวกรองการพับ / ลดและอื่น ๆ อีกมากมาย

หากคุณต้องแก้ไขรายการเก่าเพียงรวบรวมรายการที่แมปไว้ในรายการใหม่:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

แล้วทำlist.addAll(newList)- อีกครั้ง: ถ้าคุณต้องจริงๆ

(หรือสร้างรายการใหม่ที่เชื่อมโยงกับรายการเก่าและรายการใหม่และกำหนดกลับไปที่listตัวแปร - นี่เป็นจิตวิญญาณของ FP มากกว่าเล็กน้อยaddAll )

สำหรับ API: ถึงแม้ว่า API อนุญาต (อีกครั้งดูคำตอบของ assylias) คุณควรพยายามหลีกเลี่ยงการทำเช่นนั้นโดยไม่คำนึงถึงอย่างน้อยโดยทั่วไป เป็นการดีที่สุดที่จะไม่ต่อสู้กับกระบวนทัศน์ (FP) และพยายามที่จะเรียนรู้มากกว่าต่อสู้ (แม้ว่า Java โดยทั่วไปไม่ใช่ภาษา FP) และใช้กลยุทธ์ "สกปรก" หากจำเป็นจริงๆ

คำตอบที่ยาวมาก: (เช่นถ้าคุณรวมความพยายามในการค้นหาและอ่านคำนำ FP / หนังสือตามที่แนะนำ)

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


@assylias: เหตุผลมันก็ไม่ผิดเพราะก็มีหรือบางส่วน อย่างไรก็ตามได้เพิ่มบันทึก
Erik Kaplun

1
คำตอบสั้น ๆ ถูกต้อง สมุทรหนึ่งที่นำเสนอจะประสบความสำเร็จในกรณีที่เรียบง่าย แต่ล้มเหลวในกรณีทั่วไป
Stuart Marks

คำตอบที่ยาวกว่านั้นส่วนใหญ่จะถูก แต่การออกแบบของ API นั้นเกี่ยวกับการขนาน แม้ว่าแน่นอนมีหลายสิ่งเกี่ยวกับ FP ที่คล้อยตามการขนานดังนั้นแนวคิดทั้งสองนี้สอดคล้องกัน
Stuart Marks

@ StuartMarks: น่าสนใจ: ในกรณีใดบ้างคำตอบที่ให้ไว้ในคำตอบของ assylias? (และจุดที่ดีเกี่ยวกับความเท่าเทียม - ฉันคิดว่าฉันกระตือรือร้นที่จะสนับสนุน FP)
Erik Kaplun

@ErikAllik ฉันได้เพิ่มคำตอบที่ครอบคลุมปัญหานี้
Stuart Marks

11

Erik Allikได้ให้เหตุผลที่ดีอยู่แล้วทำไมคุณมักจะไม่ต้องการรวบรวมองค์ประกอบของสตรีมในรายการที่มีอยู่

อย่างไรก็ตามคุณสามารถใช้หนึ่งซับต่อไปนี้หากคุณต้องการฟังก์ชั่นนี้จริงๆ

แต่ตามที่Stuart Marksอธิบายไว้ในคำตอบของเขาคุณไม่ควรทำเช่นนี้หากกระแสข้อมูลอาจเป็นลำธารขนาน - ใช้ความเสี่ยงของคุณเอง ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));

อ๊ะนั่นเป็นความอัปยศ: P
Erik Kaplun

2
เทคนิคนี้จะล้มเหลวอย่างน่ากลัวหากกระแสข้อมูลทำงานแบบขนาน
Stuart Marks

1
มันจะเป็นความรับผิดชอบของผู้ให้บริการคอลเลกชันเพื่อให้แน่ใจว่ามันจะไม่ล้มเหลว - เช่นโดยการรวบรวมคอลเลกชันพร้อมกัน
Balder

2
ไม่รหัสนี้ละเมิดข้อกำหนดของ toCollection () ซึ่งซัพพลายเออร์ส่งคืนคอลเลกชันใหม่ที่ว่างเปล่าของประเภทที่เหมาะสม แม้ว่าปลายทางจะปลอดภัยสำหรับเธรดการผสานที่ทำเพื่อตัวพิมพ์แบบขนานจะทำให้เกิดผลลัพธ์ที่ไม่ถูกต้อง
Stuart Marks

1
@Balder ฉันได้เพิ่มคำตอบที่ควรชี้แจงนี้
Mark Stuart

4

คุณเพียงแค่ต้องอ้างอิงรายการเดิมของคุณให้เป็นรายการที่Collectors.toList()ส่งคืน

นี่คือตัวอย่าง:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

และนี่คือวิธีที่คุณสามารถเพิ่มองค์ประกอบที่สร้างขึ้นใหม่ลงในรายการต้นฉบับของคุณในหนึ่งบรรทัด

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

นั่นคือสิ่งที่กระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชั่นนี้มอบให้


ฉันหมายถึงว่าจะเพิ่ม / รวบรวมลงในรายการที่มีอยู่ไม่ใช่แค่กำหนดใหม่
codefx

1
ในทางเทคนิคแล้วคุณไม่สามารถทำสิ่งนั้นในกระบวนทัศน์การเขียนโปรแกรมเชิงปฏิบัติการซึ่งลำธารเป็นเรื่องเกี่ยวกับ ในฟังก์ชั่นการเขียนโปรแกรมสถานะจะไม่ถูกแก้ไข แต่สถานะใหม่จะถูกสร้างขึ้นในโครงสร้างข้อมูลถาวรทำให้ปลอดภัยสำหรับการใช้งานพร้อมกันและการทำงานที่มากกว่า วิธีที่ฉันกล่าวถึงคือสิ่งที่คุณสามารถทำได้หรือคุณสามารถหันไปใช้วิธีการเชิงวัตถุแบบเก่าที่คุณทำซ้ำในแต่ละองค์ประกอบและเก็บหรือลบองค์ประกอบตามที่คุณเห็นว่าเหมาะสม
Aman Agnihotri


0

ฉันจะต่อเชื่อมรายการเก่าและรายการใหม่เป็นสตรีมและบันทึกผลลัพธ์ไปยังรายการปลายทาง ทำงานได้ดีในแบบคู่ขนานอีกด้วย

ฉันจะใช้ตัวอย่างของคำตอบที่ได้รับการยอมรับโดย Stuart Marks:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

หวังว่ามันจะช่วย

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