Javadoc of Collectorแสดงวิธีรวบรวมองค์ประกอบของสตรีมในรายการใหม่ มีหนึ่งซับที่เพิ่มผลลัพธ์ลงใน ArrayList ที่มีอยู่หรือไม่
Javadoc of Collectorแสดงวิธีรวบรวมองค์ประกอบของสตรีมในรายการใหม่ มีหนึ่งซับที่เพิ่มผลลัพธ์ลงใน ArrayList ที่มีอยู่หรือไม่
คำตอบ:
หมายเหตุ: 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))
แน่นอนคุณจำได้ว่าต้องทำเช่นนี้ทุกครั้งใช่ไหม :-) สมมติว่าคุณทำ จากนั้นทีมงานจะสงสัยว่าทำไมการใช้งานแบบขนานอย่างรอบคอบทั้งหมดของพวกเขาไม่ได้ให้การเร่งความเร็วใด ๆ และอีกครั้งพวกเขาจะติดตามไปที่รหัสของคุณซึ่งบังคับให้สตรีมทั้งหมดทำงานตามลำดับ
อย่าทำมัน
toCollection
วิธีการควรกลับคอลเลกชันใหม่และว่างเปล่าในแต่ละครั้งที่โน้มน้าวให้ฉันไม่ ฉันต้องการทำลายสัญญา javadoc ของคลาส Java หลัก
forEachOrdered
หากคุณต้องการกระแสที่จะมีผลข้างเคียงที่คุณเกือบจะแน่นอนต้องการที่จะใช้ ผลข้างเคียงรวมถึงการเพิ่มองค์ประกอบให้กับคอลเลกชันที่มีอยู่ไม่ว่าจะมีองค์ประกอบอยู่แล้วก็ตาม หากคุณต้องการองค์ประกอบกระแสของวางลงใหม่คอลเลกชันการใช้งานcollect(Collectors.toList())
หรือหรือtoSet()
toCollection()
เท่าที่ฉันเห็นคำตอบอื่น ๆ ทั้งหมดนั้นใช้ตัวรวบรวมเพื่อเพิ่มองค์ประกอบให้กับสตรีมที่มีอยู่ อย่างไรก็ตามมีวิธีแก้ปัญหาที่สั้นกว่าและใช้ได้กับทั้งลำาดับต่อเนื่องและลำาดับขนาน คุณสามารถใช้วิธีการสำหรับแต่ละคำสั่งร่วมกับการอ้างอิงวิธีการ
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
ข้อ จำกัด เพียงอย่างเดียวคือแหล่งที่มาและเป้าหมายนั้นเป็นรายการที่แตกต่างกันเนื่องจากคุณไม่ได้รับอนุญาตให้ทำการเปลี่ยนแปลงแหล่งที่มาของกระแสข้อมูลตราบใดที่มีการประมวลผล
โปรดทราบว่าวิธีนี้ใช้ได้กับทั้งลำาดับต่อเนื่องและลำาดับขนาน อย่างไรก็ตามมันไม่ได้รับประโยชน์จากการเห็นพ้องด้วย การอ้างอิงวิธีการที่ส่งไปยังforEachOrderedจะถูกดำเนินการตามลำดับเสมอ
forEach(existing::add)
ความเป็นไปได้ในการอื่นคำตอบสองเดือนที่ผ่านมา ฉันควรจะเพิ่มforEachOrdered
เช่นกัน ...
forEachOrdered
แทนforEach
?
forEachOrdered
ทำงานให้กับทั้งสองตามลำดับและขนานลำธาร ในทางตรงกันข้ามforEach
อาจจะดำเนินการวัตถุฟังก์ชั่นพร้อมกันผ่านลำธารขนาน Vector<Integer>
ในกรณีนี้วัตถุฟังก์ชั่นที่จะต้องตรงกันอย่างถูกต้องเช่นโดยการใช้
target::add
ครั้ง โดยไม่คำนึงถึงจากการที่หัวข้อวิธีการที่จะเรียกไม่มีการแข่งขันข้อมูล ฉันคาดหวังให้คุณรู้ว่า
คำตอบสั้น ๆคือไม่ (หรือควรจะไม่ใช่) แก้ไข:ใช่เป็นไปได้ (ดูคำตอบของ 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 / หนังสือตามที่แนะนำ)
เพื่อหาสาเหตุที่การแก้ไขรายการที่มีอยู่โดยทั่วไปเป็นแนวคิดที่ไม่ดีและนำไปสู่รหัสที่สามารถบำรุงรักษาได้น้อยกว่า - เว้นแต่คุณจะแก้ไขตัวแปรในเครื่องและอัลกอริทึมของคุณสั้นและ / หรือไม่สำคัญซึ่งอยู่นอกขอบเขตของคำถาม - ค้นหาข้อมูลเบื้องต้นเกี่ยวกับฟังก์ชั่นการเขียนโปรแกรม (มีหลายร้อย) และเริ่มอ่าน คำอธิบาย "ตัวอย่าง" จะเป็นอย่างไร: มีเสียงทางคณิตศาสตร์และเหตุผลที่ง่ายกว่าที่จะไม่แก้ไขข้อมูล (ในส่วนของโปรแกรมของคุณ) และนำไปสู่ระดับที่สูงขึ้นและเทคนิคน้อยลง (รวมทั้งเป็นมิตรกับมนุษย์มากขึ้นเมื่อสมองของคุณ เปลี่ยนจากการคิดแบบบังคับแบบเก่า) คำจำกัดความของโปรแกรมตรรกะ
Erik Allikได้ให้เหตุผลที่ดีอยู่แล้วทำไมคุณมักจะไม่ต้องการรวบรวมองค์ประกอบของสตรีมในรายการที่มีอยู่
อย่างไรก็ตามคุณสามารถใช้หนึ่งซับต่อไปนี้หากคุณต้องการฟังก์ชั่นนี้จริงๆ
แต่ตามที่Stuart Marksอธิบายไว้ในคำตอบของเขาคุณไม่ควรทำเช่นนี้หากกระแสข้อมูลอาจเป็นลำธารขนาน - ใช้ความเสี่ยงของคุณเอง ...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
คุณเพียงแค่ต้องอ้างอิงรายการเดิมของคุณให้เป็นรายการที่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())
);
นั่นคือสิ่งที่กระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชั่นนี้มอบให้
TARGETLIST = sourceList.stream () flatmap (รายชื่อ :: กระแส) .collect (Collectors.toList ()).
ฉันจะต่อเชื่อมรายการเก่าและรายการใหม่เป็นสตรีมและบันทึกผลลัพธ์ไปยังรายการปลายทาง ทำงานได้ดีในแบบคู่ขนานอีกด้วย
ฉันจะใช้ตัวอย่างของคำตอบที่ได้รับการยอมรับโดย 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]
หวังว่ามันจะช่วย
Collection
”