คำแนะนำเกี่ยวกับ Stream.reduce ()

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