Mục lục
Stream API cung cấp rất nhiều các tính năng thuộc các nhóm intermediate, reduction, và terminal function, ngoài ra chúng cũng được hỗ trợ chạy song song nhầm tối ưu hoá hiệu suất cho chương trình.
Stream reduction operation cho phép chúng ta tạo ra một kết quả duy nhất từ các phần tử của Stream bằng cách áp dụng các phép tính lặp đi lặp lại trên các phần tử của nó.
Trong bài viết này, chúng ta sẽ tìm hiểu về Stream.reduce() là 1 reduction operation, và một số trường hợp sử dụng căn bản.
Các khái niệm cơ bản trong Reduce
Trong Stream.reduce() method chúng ta cần quan tâm đến 3 khái niệm chính:
- Identity: Giá trị khởi tạo của Reduction operation và cũng là giá trị mặc định khi Stream rỗng.
- Accumulator: Implement của BiFunction functional interface, nhận vào 2 tham số và trả về 1 kết hợp khi thực hiện trên 2 phần tử liên tiếp.
- Combiner: Implement của BinaryOperator sử dụng để kết hợp các giá trị đơn lẽ 2 từ luồng chạy song song.
Sử dụng Stream.reduce()
Trước tiên chúng ta sẽ đi qua ví dụ đơn giản về cách sử dụng Stream.reduce() trên Stream đơn luồng. Như vậy chúng ta sẽ không đến Combiner function.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); int result = numbers .stream() .reduce(0, (subtotal, element) -> subtotal + element); // 21
Trong ví dụ trên:
- Tham số đầu tiên, số interger = 0 là identity, nó là giá ban đầu của reduction operation. Mặc khác nó cũng là giá trị mặc định khi Stream rỗng.
- Tham số thứ 2 là một lambda expression triển khai BiFunction functional interface tương ứng với accumulator, nó sẽ lấy tổng của các phần tử trước đó cộng với phần tử tiếp theo cho đến phần tử cuối cùng của Stream.
Ngoài ra chúng ta có thể làm cho code ngắn gọn hơn một chút xíu với việc thay thế lambda expression bằng method reference.
Một ví dụ khác trên String, cộng dồn tất cả các String trong List với nhau.
List<String> letters = Arrays.asList("a", "b", "c", "d", "e"); String result = letters .stream() .reduce("", (partialString, element) -> partialString + element); // abcde
Tương tự chúng ta có thể thay thế bằng method reference với concat() method
String result = letters.stream().reduce("", String::concat);
Stream.reduce() trong Parallel
Khi một Stream thực thi trong chế động song song (Parallel), Java runtime sẽ tách Stream ban đầu thành nhiều substreams cùng thực thi. Do vậy chúng ta cùng một combiner method để kết hợp các giá trị từ các substreams lại với nhau.
List<Integer> ages = Arrays.asList(25, 30, 45, 28, 32); int computedAges = ages.parallelStream().reduce(0, (a, b) -> a + b, Integer::sum);
Khi sử dụng Stream.reduce() trong Stream Parallel, không chỉ reduce() mà các method khác tham gia vào Stream Parallel phải đảm bảo các điều khoản sau:
- associative: kết quả không bị ảnh hưởng bởi thứ tự các phép toán.
- non-interfering: các toán hạng không làm ảnh hưởng đến dữ liệu nguồn.
- stateless & deterministic: các toán hạng không được phép lưu trữ trạng thái và cho cùng một kết quả trên các dữ liệu đầu vào giống nhau.
Dĩ nhiên là Stream Parallel sẽ cho hiệu năng tốt hơn so với Stream tuần tự. Tuy nhiên nếu số phần tử của Stream quá nhỏ thì chúng ta không cần thiết phải sử dụng.
Xử lý exception trong Reduce()
Trong thực tế, thì dữ liệu đầu vào phức tạp hơn nhiều so với các ví dụ của chúng ta. Ví dụ viết 1 method sumOfDiv() dùng để tính tổng các phần tử, nhưng trước tiên chúng phải được chia cho 1 số divider cho trước.
public static int sumOfDiv(List<Integer> numbers, int divider) { return numbers.stream().reduce(0, (a, b) -> a / divider + b / divider); }
Nếu chúng ta truyền divider là số 0 thì chương trình sẽ ném ra ArithmeticException, vì 1 số không thể chia cho 0.
Chúng ta có thể sử dụng try-catch để xử lý khi có exception xảy ra, trong ví dụ này, mình sẽ log ra màn hình exception và trả về giá trị 0 khi có exception xảy ra.
public class test { public static void main(String[] agrs) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); System.out.println("Sum:" + sumOfDiv(numbers, 0)); } public static int sumOfDiv(List<Integer> numbers, int divider) { return numbers.stream() .reduce(0, (a, b) -> { try { return a / divider + b / divider; } catch (ArithmeticException e) { System.out.println("Arithmetic Exception: Division by Zero"); } return 0; }); }
Output:
Arithmetic Exception: Division by Zero
Arithmetic Exception: Division by Zero
Arithmetic Exception: Division by Zero
Arithmetic Exception: Division by Zero
Arithmetic Exception: Division by Zero
Arithmetic Exception: Division by Zero
Sum: 0
Stream.reduce() với User Object
Nảy giờ chúng ta lấy các ví dụ cho các kiểu dữ liệu thông thường, vậy reduce() khi làm việc với các Object do chúng ta tự định nghĩa thì sẽ như thế nào đây.
Giả sử chúng ta triển khai hệ thống đánh giá bài viết với 2 đối tượng chính là Review và Rating. Rating sẽ có một List các Review với 1 chuỗi bình luận và số điểm được cho, từ đó Rating có thể tính được điểm trung bình hoặc tính tổng điểm được cho etc.
public class Review { private int points; private String review; // constructor, getters and setters }
public class Rating { private double points; private List<Review> 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; } // constructor, getters and setters }
Để cụ thể hơn chúng ta có một Product class, nó sẽ có 1 thuộc tính là Rating object đại diện cho những đáng của một sản phẩm cụ thể.
public class Product { private String name; private int price; private Rating rating; public Product(String name, int price) { this.name = name; this.price = price; this.rating = new Rating(); } // getter, setter method }
Sau đây, chúng ta sẽ tiến hành kiểm tra bằng cách tạo ra một List Product và thêm các Review cho chúng. Cuối cùng là tính trung bình điểm Review cho các Product để đánh giá mặt bằng chung các sản phẩm của công ty.
public class test { public static void main(String[] agrs) { Product a = new Product("Product A", 30); a.getRating().add(new Review(5, "")); a.getRating().add(new Review(3, "not bad")); Product b = new Product("Product A", 100); b.getRating().add(new Review(4, "great!")); b.getRating().add(new Review(2, "terrible experience")); b.getRating().add(new Review(4, "")); List<Product> products = Arrays.asList(a, b); Rating averageRating = products.stream().reduce(new Rating(), (rating, user) -> Rating.average(rating, user.getRating()), Rating::average); System.out.println("Point: " + averageRating.getPoints()); // PPoint: 3.6 } }
Tóm lược
Stream.reduce() được sử dụng để mô tả các hành động lặp đi lại lại tương tự nhau trên mỗi phần tử của một tập dữ liệu. Giúp giảm thiểu code đáng kể so với cách viết mã truyền thống, chúng ta có thể nhìn vào reduce() function để có thể thấy được cái nhìn tổng quan về nhiệm vụ mà nó đang thực hiệm
Nguồn tham khảo