เรารอมานานแล้วที่ lambda จะนำแนวคิดการปิดตัวมาสู่ Java แต่ถ้าเราไม่ใช้มันในคอลเลกชั่น เราก็จะสูญเสียคุณค่าไปมาก ปัญหาของการย้ายอินเทอร์เฟซที่มีอยู่ไปเป็นสไตล์แลมบ์ดาได้รับการแก้ไขแล้วด้วยวิธีการเริ่มต้น ในบทความนี้ เราจะวิเคราะห์การดำเนินการข้อมูลจำนวนมาก (การดำเนินการจำนวนมาก) ในคอลเลกชัน Java อย่างลึกซึ้ง และไขปริศนาของบทบาทที่ทรงพลังที่สุดของแลมบ์ดา
1.เกี่ยวกับ JSR335
JSR เป็นตัวย่อของ Java Specification Requests ซึ่งหมายถึงคำขอข้อกำหนด Java การปรับปรุงหลักของเวอร์ชัน Java 8 คือโครงการ Lambda (JSR 335) ซึ่งมีจุดมุ่งหมายเพื่อให้ Java เขียนโค้ดสำหรับโปรเซสเซอร์แบบมัลติคอร์ได้ง่ายขึ้น JSR 335=นิพจน์แลมบ์ดา + การปรับปรุงอินเทอร์เฟซ (วิธีการเริ่มต้น) + การดำเนินการข้อมูลแบทช์ เมื่อรวมกับสองบทความก่อนหน้านี้ เราได้เรียนรู้เนื้อหาที่เกี่ยวข้องของ JSR335 อย่างสมบูรณ์
2. การวนซ้ำภายนอกและภายใน
ในอดีต คอลเลกชั่น Java ไม่สามารถแสดงการวนซ้ำภายในได้ แต่จัดให้มีวิธีเดียวในการวนซ้ำภายนอก นั่นคือ for หรือ while loop
คัดลอกรหัสรหัสดังต่อไปนี้:
รายชื่อบุคคล = asList (บุคคลใหม่ ("โจ") บุคคลใหม่ ("จิม") บุคคลใหม่ ("จอห์น"));
สำหรับ (บุคคล p : บุคคล) {
p.setLastName("โด้");
-
ตัวอย่างข้างต้นคือแนวทางก่อนหน้าของเรา ซึ่งเรียกว่าการวนซ้ำภายนอก ในยุค Multi-core ในปัจจุบัน หากเราต้องการวนซ้ำแบบขนาน เราต้องแก้ไขโค้ดข้างต้น ความสามารถในการปรับปรุงประสิทธิภาพนั้นยังไม่เป็นที่แน่ชัด และจะนำมาซึ่งความเสี่ยง (ปัญหาด้านความปลอดภัยของเกลียว ฯลฯ)
เพื่ออธิบายการวนซ้ำภายใน เราจำเป็นต้องใช้คลาสไลบรารีเช่น Lambda มาเขียนลูปด้านบนใหม่โดยใช้ lambda และ Collection.forEach
คัดลอกรหัสดังนี้: Person.forEach(p->p.setLastName("Doe"));
ตอนนี้ไลบรารี่ jdk ควบคุมลูป เราไม่จำเป็นต้องสนใจว่านามสกุลจะถูกตั้งค่าให้กับวัตถุของแต่ละคนอย่างไร ไลบรารีสามารถตัดสินใจได้ว่าจะต้องทำอย่างไรตามสภาพแวดล้อมที่ทำงานอยู่ แบบขนาน ไม่อยู่ในลำดับ หรือขี้เกียจ กำลังโหลด นี่คือการวนซ้ำภายใน และไคลเอ็นต์ส่งพฤติกรรม p.setLastName เป็นข้อมูลไปยัง API
ในความเป็นจริง การวนซ้ำภายในไม่เกี่ยวข้องอย่างใกล้ชิดกับการดำเนินการแบบกลุ่มของคอลเลกชัน ด้วยความช่วยเหลือนี้ เราจึงสัมผัสได้ถึงการเปลี่ยนแปลงในการแสดงออกทางไวยากรณ์ สิ่งที่น่าสนใจจริงๆ ที่เกี่ยวข้องกับการดำเนินการแบบแบตช์คือ API สตรีมใหม่ มีการเพิ่มแพ็คเกจ java.util.stream ใหม่ลงใน JDK 8
3.สตรีม API
สตรีมแสดงถึงการไหลของข้อมูลเท่านั้นและไม่มีโครงสร้างข้อมูล ดังนั้นจึงไม่สามารถสำรวจได้อีกต่อไปหลังจากที่มีการสำรวจเพียงครั้งเดียว (ซึ่งจะต้องให้ความสนใจเมื่อเขียนโปรแกรม ซึ่งแตกต่างจาก Collection ซึ่งยังคงมีข้อมูลอยู่ในนั้นไม่ว่าจะกี่ครั้งก็ตาม มันถูกสำรวจ) แหล่งที่มาสามารถเป็น Collection, array, io ฯลฯ
3.1 วิธีขั้นกลางและจุดสิ้นสุด
การสตรีมเป็นอินเทอร์เฟซสำหรับการดำเนินงานข้อมูลขนาดใหญ่ ทำให้การดำเนินงานข้อมูลง่ายขึ้นและเร็วขึ้น มีวิธีการต่างๆ เช่น การกรอง การแม็ป และการลดจำนวนเส้นทาง วิธีการเหล่านี้แบ่งออกเป็น 2 ประเภท ได้แก่ วิธีการระดับกลาง และวิธีเทอร์มินัล ดังนั้น หากเป็นนามธรรม เราต้องการได้รับผลลัพธ์สุดท้าย หากเป็นเช่นนั้น การดำเนินการปลายทางจะต้องถูกนำมาใช้เพื่อรวบรวมผลลัพธ์สุดท้ายที่สร้างโดยสตรีม ความแตกต่างระหว่างสองวิธีนี้คือการดูค่าที่ส่งคืน หากเป็น Stream จะเป็นวิธีการระดับกลาง ไม่เช่นนั้นจะเป็นวิธีการสิ้นสุด โปรดดูรายละเอียดที่ API ของสตรีม
แนะนำวิธีการระดับกลางหลายวิธีโดยย่อ (ตัวกรอง แผนที่) และวิธีการจุดสิ้นสุด (รวบรวม ผลรวม)
3.1.1ตัวกรอง
การใช้ฟังก์ชันการกรองในสตรีมข้อมูลเป็นการดำเนินการที่เป็นธรรมชาติที่สุดที่เราคิดได้ อินเทอร์เฟซ Stream เปิดเผยวิธีการกรอง ซึ่งยอมรับการใช้งานภาคแสดงที่แสดงถึงการดำเนินการเพื่อใช้นิพจน์แลมบ์ดาที่กำหนดเงื่อนไขตัวกรอง
คัดลอกรหัสรหัสดังต่อไปนี้:
รายชื่อบุคคล = …
สตรีม PersonOver18 = Persons.stream().filter(p -> p.getAge() > 18);//กรองผู้ที่มีอายุ 18 ปีขึ้นไป
3.1.2แผนที่
สมมติว่าตอนนี้เรากรองข้อมูลบางอย่าง เช่น เมื่อแปลงออบเจ็กต์ การดำเนินการแผนที่ช่วยให้เราสามารถดำเนินการใช้งานฟังก์ชั่น (T และ R ทั่วไปของฟังก์ชั่น <T, R> แสดงถึงอินพุตการดำเนินการและผลลัพธ์การดำเนินการตามลำดับ) ซึ่งยอมรับพารามิเตอร์อินพุตและส่งกลับ ขั้นแรก เรามาดูวิธีอธิบายว่าเป็นคลาสภายในที่ไม่เปิดเผยตัวตน:
คัดลอกรหัสรหัสดังต่อไปนี้:
สตรีมผู้ใหญ่=บุคคล
.ลำธาร()
.filter(p -> p.getAge() > 18)
.map(ฟังก์ชั่นใหม่() {
@แทนที่
สมัครสำหรับผู้ใหญ่สาธารณะ (บุคคล) {
return new Adult(person);//แปลงบุคคลที่อายุมากกว่า 18 ปีเป็นผู้ใหญ่
-
-
ตอนนี้ แปลงตัวอย่างข้างต้นเป็นนิพจน์แลมบ์ดา:
คัดลอกรหัสรหัสดังต่อไปนี้:
แผนที่สตรีม = Persons.stream()
.filter(p -> p.getAge() > 18)
.map(บุคคล -> ผู้ใหญ่ใหม่(บุคคล));
3.1.3นับ
วิธีการนับเป็นวิธีการสิ้นสุดของสตรีม ซึ่งสามารถสร้างสถิติสุดท้ายของผลลัพธ์ของสตรีมและส่งกลับค่า int ได้ ตัวอย่างเช่น ลองคำนวณจำนวนคนทั้งหมดที่มีอายุ 18 ปีขึ้นไป:
คัดลอกรหัสรหัสดังต่อไปนี้:
int countOfAdult=persons.stream()
.filter(p -> p.getAge() > 18)
.map(คน -> ผู้ใหญ่ใหม่(คน))
.นับ();
3.1.4การรวบรวม
วิธีการรวบรวมยังเป็นวิธีการจุดสิ้นสุดของสตรีม ซึ่งสามารถรวบรวมผลลัพธ์สุดท้ายได้
คัดลอกรหัสรหัสดังต่อไปนี้:
รายการ adultList= person.stream()
.filter(p -> p.getAge() > 18)
.map(คน -> ผู้ใหญ่ใหม่(คน))
.collect(Collectors.toList());
หรือหากเราต้องการใช้คลาสการใช้งานเฉพาะเพื่อรวบรวมผลลัพธ์:
คัดลอกรหัสรหัสดังต่อไปนี้:
รายชื่อ adultList = บุคคล
.ลำธาร()
.filter(p -> p.getAge() > 18)
.map(คน -> ผู้ใหญ่ใหม่(คน))
.collect(Collectors.toCollection(ArrayList::new));
เนื่องจากมีพื้นที่จำกัด วิธีการขั้นกลางและวิธีการปลายทางอื่นๆ จะไม่ถูกนำมาใช้ทีละรายการ หลังจากอ่านตัวอย่างข้างต้นแล้ว คุณเพียงแค่ต้องเข้าใจความแตกต่างระหว่างสองวิธีนี้เท่านั้น และคุณสามารถตัดสินใจใช้งานได้ตามความต้องการของคุณ ภายหลัง.
3.2 การไหลตามลำดับและการไหลแบบขนาน
แต่ละสตรีมมีสองโหมด: การดำเนินการตามลำดับและการดำเนินการแบบขนาน
ลำดับโฟลว์:
คัดลอกรหัสรหัสดังต่อไปนี้:
รายการ <บุคคล> คน = list.getStream.collect(Collectors.toList());
กระแสขนาน:
คัดลอกรหัสรหัสดังต่อไปนี้:
รายการ <บุคคล> คน = list.getStream.parallel().collect(Collectors.toList());
ดังที่ชื่อบอกไว้ เมื่อใช้วิธีการเรียงลำดับเพื่อสำรวจ แต่ละรายการจะถูกอ่านก่อนที่จะอ่านรายการถัดไป เมื่อใช้การเคลื่อนที่แบบขนาน อาร์เรย์จะถูกแบ่งออกเป็นหลายส่วน ซึ่งแต่ละส่วนจะถูกประมวลผลในเธรดที่แตกต่างกัน จากนั้นผลลัพธ์จะถูกส่งออกพร้อมกัน
3.2.1 หลักการกระแสขนาน:
คัดลอกรหัสรหัสดังต่อไปนี้:
รายการ originalList = someData;
split1 = originalList(0, mid);//แบ่งข้อมูลออกเป็นส่วนเล็กๆ
split2 = รายการต้นฉบับ (กลาง, ท้าย);
new Runnable(split1.process());//ดำเนินการดำเนินการในส่วนเล็กๆ
ใหม่ Runnable(split2.process());
รายการแก้ไขรายการ = split1 + split2;//รวมผลลัพธ์
3.2.2 การเปรียบเทียบการทดสอบประสิทธิภาพแบบต่อเนื่องและแบบขนาน
หากเป็นเครื่องแบบมัลติคอร์ ในทางทฤษฎีแล้ว สตรีมแบบขนานจะเร็วเป็นสองเท่าของสตรีมตามลำดับ ต่อไปนี้คือโค้ดทดสอบ
คัดลอกรหัสรหัสดังต่อไปนี้:
ยาว t0 = System.nanoTime();
//เริ่มต้นสตรีมจำนวนเต็มด้วยช่วง 1 ล้านและค้นหาตัวเลขที่สามารถหารด้วย 2 ได้ toArray() เป็นวิธีจุดสิ้นสุด
int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
ยาว t1 = System.nanoTime();
//ฟังก์ชันเดียวกันกับข้างบน ในที่นี้เราใช้สตรีมแบบขนานในการคำนวณ
int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
ยาว t2 = System.nanoTime();
// ผลลัพธ์ของเครื่องท้องถิ่นของฉันคืออนุกรม: 0.06 วินาที, ขนาน 0.02 วินาที ซึ่งพิสูจน์ว่าการไหลแบบขนานนั้นเร็วกว่าการไหลตามลำดับอย่างแน่นอน
System.out.printf("อนุกรม: %.2fs, ขนาน %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);
3.3 เกี่ยวกับกรอบงาน Folk/Join
ความเท่าเทียมของฮาร์ดแวร์แอปพลิเคชันมีอยู่ใน Java 7 หนึ่งในคุณสมบัติใหม่ของแพ็คเกจ java.util.concurrent คือเฟรมเวิร์กการแยกส่วนแบบขนานแบบ fork-join นอกจากนี้ยังมีประสิทธิภาพและประสิทธิผลมาก นักเรียนที่สนใจสามารถศึกษาได้ เข้าไปดูรายละเอียดที่นี่ เมื่อเทียบกับ Stream.parallel() ฉันชอบอันหลังมากกว่า
4. สรุป
หากไม่มีแลมบ์ดา Stream จะค่อนข้างอึดอัดในการใช้งาน มันจะสร้างคลาสภายในที่ไม่ระบุชื่อจำนวนมาก เช่นตัวอย่างแผนที่ 3.1.2 ด้านบน หากไม่มีวิธีการเริ่มต้น การเปลี่ยนแปลงในกรอบงานการรวบรวมจะทำให้เกิดการเปลี่ยนแปลงมากมายอย่างหลีกเลี่ยงไม่ได้ ดังนั้นเมธอด lambda+default ทำให้ไลบรารี jdk มีประสิทธิภาพและยืดหยุ่นมากขึ้น การปรับปรุง Stream และเฟรมเวิร์กการรวบรวมจึงเป็นข้อพิสูจน์ที่ดีที่สุด