1. ภาพรวม
Stream API มีฟังก์ชันการทำงานระดับกลางการลดและเทอร์มินัลซึ่งรองรับการทำงานแบบขนาน
โดยเฉพาะอย่างยิ่งการดำเนินการสตรีมการลดลงช่วยให้เราสามารถสร้างผลลัพธ์เดียวจากลำดับขององค์ประกอบโดยใช้การดำเนินการรวมซ้ำ ๆ กับองค์ประกอบในลำดับ
ในบทช่วยสอนนี้เราจะดูการดำเนินการStream.reduce ()วัตถุประสงค์ทั่วไปและดูการใช้งานที่เป็นรูปธรรม
2. แนวคิดหลัก: อัตลักษณ์ตัวสะสมและ Combiner
ก่อนที่เราจะดูลึกลงไปในการใช้การดำเนินการStream.reduce ()เราจะแบ่งองค์ประกอบผู้เข้าร่วมของการดำเนินการออกเป็นบล็อกแยกกัน ด้วยวิธีนี้เราจะเข้าใจบทบาทที่แต่ละคนเล่นได้ง่ายขึ้น:
- Identity - องค์ประกอบที่เป็นค่าเริ่มต้นของการดำเนินการลดและผลลัพธ์เริ่มต้นหากสตรีมว่างเปล่า
- แอคคูมูเลเตอร์ - ฟังก์ชันที่รับสองพารามิเตอร์: ผลลัพธ์บางส่วนของการดำเนินการลดและองค์ประกอบถัดไปของสตรีม
- Combiner - ฟังก์ชันที่ใช้เพื่อรวมผลลัพธ์บางส่วนของการดำเนินการลดเมื่อการลดขนานกันหรือเมื่อมีความไม่ตรงกันระหว่างประเภทของอาร์กิวเมนต์ตัวสะสมและประเภทของการใช้งานตัวสะสม
3. การใช้Stream.reduce ()
เพื่อให้เข้าใจการทำงานขององค์ประกอบเอกลักษณ์ตัวสะสมและตัวรวมกันได้ดีขึ้นลองดูตัวอย่างพื้นฐานบางส่วน:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); int result = numbers .stream() .reduce(0, (subtotal, element) -> subtotal + element); assertThat(result).isEqualTo(21);
ในกรณีนี้จำนวนเต็มค่า 0 เป็นตัวตน มันเก็บค่าเริ่มต้นของการดำเนินการลดและยังเป็นผลลัพธ์เริ่มต้นเมื่อสตรีมของค่าจำนวนเต็มว่างเปล่า
ในทำนองเดียวกันการแสดงออกของแลมบ์ดา :
subtotal, element -> subtotal + element
เป็นตัวสะสมเนื่องจากใช้ผลรวมบางส่วนของค่าจำนวนเต็มและองค์ประกอบถัดไปในสตรีม
เพื่อให้โค้ดมีความกระชับมากยิ่งขึ้นเราสามารถใช้การอ้างอิงเมธอดแทนนิพจน์แลมด้า:
int result = numbers.stream().reduce(0, Integer::sum); assertThat(result).isEqualTo(21);
แน่นอนเราสามารถใช้การดำเนินการลด ()บนสตรีมที่มีองค์ประกอบประเภทอื่น ๆ
ตัวอย่างเช่นเราสามารถใช้reduce ()กับอาร์เรย์ขององค์ประกอบStringและรวมเข้าเป็นผลลัพธ์เดียว:
List letters = Arrays.asList("a", "b", "c", "d", "e"); String result = letters .stream() .reduce("", (partialString, element) -> partialString + element); assertThat(result).isEqualTo("abcde");
ในทำนองเดียวกันเราสามารถเปลี่ยนไปใช้เวอร์ชันที่ใช้การอ้างอิงวิธีการ:
String result = letters.stream().reduce("", String::concat); assertThat(result).isEqualTo("abcde");
มาใช้การดำเนินการลด ()เพื่อเข้าร่วมองค์ประกอบตัวพิมพ์ใหญ่ของอาร์เรย์ตัวอักษร :
String result = letters .stream() .reduce( "", (partialString, element) -> partialString.toUpperCase() + element.toUpperCase()); assertThat(result).isEqualTo("ABCDE");
นอกจากนี้เราสามารถใช้การลด ()ในสตรีมแบบขนาน (เพิ่มเติมในภายหลัง):
List ages = Arrays.asList(25, 30, 45, 28, 32); int computedAges = ages.parallelStream().reduce(0, a, b -> a + b, Integer::sum);
เมื่อสตรีมทำงานแบบขนานรันไทม์ Java จะแยกสตรีมออกเป็นสตรีมย่อยหลายรายการ ในกรณีเช่นนี้เราจำเป็นต้องใช้ฟังก์ชั่นที่จะรวมผลการ substreams ที่เป็นหนึ่งเดียว นี่คือบทบาทของตัวผสม - ในส่วนย่อยด้านบนเป็นการอ้างอิงเมธอดInteger :: sum
ตลกพอรหัสนี้จะไม่รวบรวม:
List users = Arrays.asList(new User("John", 30), new User("Julie", 35)); int computedAges = users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge());
ในกรณีนี้เรามีสตรีมของอ็อบเจ็กต์ผู้ใช้และประเภทของอาร์กิวเมนต์ตัวสะสมคือจำนวนเต็มและผู้ใช้ อย่างไรก็ตามการใช้งานตัวสะสมเป็นผลรวมของจำนวนเต็มดังนั้นคอมไพเลอร์จึงไม่สามารถสรุปประเภทของพารามิเตอร์ผู้ใช้ได้
เราสามารถแก้ไขปัญหานี้ได้โดยใช้ตัวรวม:
int result = users.stream() .reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum); assertThat(result).isEqualTo(65);
พูดง่ายๆก็คือถ้าเราใช้สตรีมตามลำดับและประเภทของอาร์กิวเมนต์ตัวสะสมและประเภทของการจับคู่การนำไปใช้งานเราไม่จำเป็นต้องใช้ตัวรวมกัน
4. การลดแบบขนาน
ดังที่เราได้เรียนไปแล้วเราสามารถใช้การลด ()กับสตรีมแบบขนาน
เมื่อเราใช้สตรีมแบบขนานเราควรตรวจสอบให้แน่ใจว่าการลด ()หรือการดำเนินการรวมอื่น ๆ ที่ดำเนินการบนสตรีมคือ:
- เชื่อมโยง : ผลลัพธ์จะไม่ได้รับผลกระทบจากลำดับของตัวถูกดำเนินการ
- ไม่รบกวน : การดำเนินการไม่ส่งผลกระทบต่อแหล่งข้อมูล
- ไร้สถานะและกำหนด : การดำเนินการไม่มีสถานะและสร้างผลลัพธ์เดียวกันสำหรับอินพุตที่กำหนด
เราควรปฏิบัติตามเงื่อนไขทั้งหมดนี้เพื่อป้องกันผลลัพธ์ที่ไม่สามารถคาดเดาได้
ตามที่คาดไว้การดำเนินการที่ดำเนินการบนสตรีมแบบขนานซึ่งรวมถึงการลด ()จะดำเนินการควบคู่กันดังนั้นการใช้ประโยชน์จากสถาปัตยกรรมฮาร์ดแวร์แบบมัลติคอร์
สำหรับเหตุผลที่ชัดเจนลำธาร parallelized มีมาก performant กว่า counterparts ถึงกระนั้นก็สามารถใช้งานได้มากเกินไปหากการดำเนินการที่ใช้กับสตรีมนั้นไม่ได้มีราคาแพงหรือองค์ประกอบในสตรีมมีจำนวนน้อย
แน่นอนว่าสตรีมแบบขนานเป็นวิธีที่ถูกต้องเมื่อเราต้องทำงานกับสตรีมขนาดใหญ่และดำเนินการรวมที่มีราคาแพง
มาสร้างการทดสอบเกณฑ์มาตรฐาน JMH (Java Microbenchmark Harness) อย่างง่ายและเปรียบเทียบเวลาการดำเนินการตามลำดับเมื่อใช้การดำเนินการลด ()บนสตรีมแบบต่อเนื่องและแบบขนาน:
@State(Scope.Thread) private final List userList = createUsers(); @Benchmark public Integer executeReduceOnParallelizedStream() { return this.userList .parallelStream() .reduce( 0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum); } @Benchmark public Integer executeReduceOnSequentialStream() { return this.userList .stream() .reduce( 0, (partialAgeResult, user) -> partialAgeResult + user.getAge(), Integer::sum); }
ในมาตรฐาน JMH ข้างต้นเราเปรียบเทียบการดำเนินการเฉลี่ยครั้ง เราเพียงแค่สร้างรายการที่มีวัตถุผู้ใช้จำนวนมาก ต่อไปเราเรียกว่าการลด ()ในลำดับและสตรีมแบบขนานและตรวจสอบว่าสตรีมหลังทำงานเร็วกว่าในอดีต (เป็นวินาทีต่อการดำเนินการ)
นี่คือผลลัพธ์มาตรฐานของเรา:
Benchmark Mode Cnt Score Error Units JMHStreamReduceBenchMark.executeReduceOnParallelizedStream avgt 5 0,007 ± 0,001 s/op JMHStreamReduceBenchMark.executeReduceOnSequentialStream avgt 5 0,010 ± 0,001 s/op
5. การขว้างปาและการจัดการข้อยกเว้นในขณะที่ลด
ในตัวอย่างข้างต้นการดำเนินการลด ()ไม่ได้ทำให้เกิดข้อยกเว้นใด ๆ แต่มันอาจจะแน่นอน
ตัวอย่างเช่นสมมติว่าเราจำเป็นต้องแบ่งองค์ประกอบทั้งหมดของสตรีมด้วยปัจจัยที่ให้มาแล้วรวมเข้าด้วยกัน:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); int divider = 2; int result = numbers.stream().reduce(0, a / divider + b / divider);
สิ่งนี้จะใช้ได้ตราบเท่าที่ตัวแปรตัวแบ่งไม่ใช่ศูนย์ แต่ถ้าเป็นศูนย์ให้ลด ()จะทำให้ข้อยกเว้นArithmeticException : หารด้วยศูนย์
เราสามารถตรวจจับข้อยกเว้นได้อย่างง่ายดายและทำสิ่งที่เป็นประโยชน์กับมันเช่นการบันทึกการกู้คืนจากมันและอื่น ๆ ขึ้นอยู่กับกรณีการใช้งานโดยใช้บล็อก try / catch:
public static int divideListElements(List values, int divider) { return values.stream() .reduce(0, (a, b) -> { try { return a / divider + b / divider; } catch (ArithmeticException e) { LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero"); } return 0; }); }
ในขณะที่วิธีการนี้จะทำงานเราเปื้อนแสดงออกแลมบ์ดากับลอง / จับบล็อก เราไม่มีซับในที่สะอาดแบบที่เคยมีมาก่อนอีกต่อไป
ในการแก้ไขปัญหานี้เราสามารถใช้เทคนิคการปรับโครงสร้างฟังก์ชันการแยกและแยกบล็อกtry / catchออกเป็นวิธีการแยก :
private static int divide(int value, int factor) { int result = 0; try { result = value / factor; } catch (ArithmeticException e) { LOGGER.log(Level.INFO, "Arithmetic Exception: Division by Zero"); } return result }
Now, the implementation of the divideListElements() method is again clean and streamlined:
public static int divideListElements(List values, int divider) { return values.stream().reduce(0, (a, b) -> divide(a, divider) + divide(b, divider)); }
Assuming that divideListElements() is a utility method implemented by an abstract NumberUtils class, we can create a unit test to check the behavior of the divideListElements() method:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); assertThat(NumberUtils.divideListElements(numbers, 1)).isEqualTo(21);
Let's also test the divideListElements() method, when the supplied List of Integer values contains a 0:
List numbers = Arrays.asList(0, 1, 2, 3, 4, 5, 6); assertThat(NumberUtils.divideListElements(numbers, 1)).isEqualTo(21);
Finally, let's test the method implementation when the divider is 0, too:
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); assertThat(NumberUtils.divideListElements(numbers, 0)).isEqualTo(0);
6. Complex Custom Objects
We can also use Stream.reduce() with custom objects that contain non-primitive fields. To do so, we need to provide a relevant identity, accumulator, and combiner for the data type.
Suppose our User is part of a review website. Each of our Users can possess one Rating, which is averaged over many Reviews.
First, let's start with our Review object. Each Review should contain a simple comment and score:
public class Review { private int points; private String review; // constructor, getters and setters }
Next, we need to define our Rating, which will hold our reviews alongside a points field. As we add more reviews, this field will increase or decrease accordingly:
public class Rating { double points; List reviews = new ArrayList(); public void add(Review review) { reviews.add(review); computeRating(); } private double computeRating() { double totalPoints = reviews.stream().map(Review::getPoints).reduce(0, Integer::sum); this.points = totalPoints / reviews.size(); return this.points; } public static Rating average(Rating r1, Rating r2) { Rating combined = new Rating(); combined.reviews = new ArrayList(r1.reviews); combined.reviews.addAll(r2.reviews); combined.computeRating(); return combined; } }
We have also added an average function to compute an average based on the two input Ratings. This will work nicely for our combiner and accumulator components.
Next, let's define a list of Users, each with their own sets of reviews.
User john = new User("John", 30); john.getRating().add(new Review(5, "")); john.getRating().add(new Review(3, "not bad")); User julie = new User("Julie", 35); john.getRating().add(new Review(4, "great!")); john.getRating().add(new Review(2, "terrible experience")); john.getRating().add(new Review(4, "")); List users = Arrays.asList(john, julie);
ตอนนี้จอห์นและจูลี่ได้รับการพิจารณาแล้วให้ใช้Stream.reduce ()เพื่อคำนวณคะแนนเฉลี่ยของผู้ใช้ทั้งสอง ในฐานะที่เป็นข้อมูลประจำตัวเราจะส่งคืนRatingใหม่หากรายการอินพุตของเราว่างเปล่า :
Rating averageRating = users.stream() .reduce(new Rating(), (rating, user) -> Rating.average(rating, user.getRating()), Rating::average);
ถ้าเราทำคณิตศาสตร์เราจะพบว่าคะแนนเฉลี่ยเท่ากับ 3.6:
assertThat(averageRating.getPoints()).isEqualTo(3.6);
7. สรุป
ในการกวดวิชานี้เราได้เรียนรู้วิธีการใช้Stream.reduce ()การดำเนินงาน นอกจากนี้เราได้เรียนรู้วิธีการดำเนินการลดลงในลำดับและลำธาร parallelized และวิธีการจัดการกับข้อยกเว้นในขณะที่ลด
ตามปกติตัวอย่างโค้ดทั้งหมดที่แสดงในบทช่วยสอนนี้มีอยู่ใน GitHub