Collectors.groupingBy method trong được giới thiệu trong Java 8, sử dụng để gom nhóm các object. Nó hoạt động tương tự như câu lệnh “GROUP BY” trong SQL, trả về một Map<key, value> với key là thuộc tính gom nhóm.
Collectors class cung cấp rất nhiều overload method của groupingBy() method.
static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
groupingBy() với 2 tham số đầu vào
static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
Hoặc là groupingBy() method với Supplier method (được sử dụng để trả về một implement của Map interface trong kết quả cuối cùng), chúng ta sẽ tìm hiểu ở các ví dụ sau.
static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
GroupingBy Collectors Example
Để tìm hiểu cách hoạt động của groupingBy() method, trước tiên chúng ta sẽ định nghĩa BlogPost class.
class BlogPost { String title; String author; BlogPostType type; int likes; // constructor, getter, setter method }
BlogPostType:
enum BlogPostType { NEWS, REVIEW, GUIDE }
Giả sử chúng ta có một danh sách các BlogPost được lưu trữ trong List.
List<BlogPost> posts = Arrays.asList( ... );
GroupingBy Single Column
Đây là trường hợp đơn giản và thường xuyên sử dụng nhất, chúng ta sẽ sử dụng
static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
Trong đó classifier function sẽ được áp dụng cho tất cả các phần tử của Stream, giá trị trả về của nó sẽ được dùng làm key của Map trong kết quả trả về, value là một ArrayList tương ứng với những phần tử có giá trị key giống nhau.
Để nhóm các BlogPost theo BlogType:
Map<BlogPostType, List<BlogPost>> postsPerType = posts.stream() .collect(groupingBy(BlogPost::getType));
GroupingBy Multiple Column
Để nhóm các đối tượng theo nhiều thuộc tính khác nhau, chúng ta có thể tạo một 1 class chứa tất cả các thuộc tính mà chúng ta cần nhóm. Cuối cùng triển khai trong groupingBy(Function<? super T,? extends K> classifier).
Tuy nhiên class này phải triển khai equals() và hashCode() method, nếu không chúng ta sẽ có một Map với tất cả các key chỉ chứa một phần tử cho dù các thuộc tính bên trong có giá trị như nhau, điều này xảy ra do mặc định Java sử dụng equals() để so sánh key, equals() method trong Object class mặc định so sánh địa chỉ vùng nhớ, nên các object sẽ hoàn toàn khác nhau.
Hiện nay, chúng ta có rất nhiều cách giải quyết vấn đề này, như sử dụng @EqualsAndHashCode annotation của lombok, hoặc sử dụng Pair, Triple của thư viện Commoms lang 3 hoặc các thư viện khác hỗ trợ chúng etc.
Để nhóm BlogPost theo BlogType và Author, trước tiên tạo BlogTypeAndAuthor class, sử dụng @EqualsAndHashCode của lombok để triển khai equals() và hashCode() tự động.
@EqualsAndHashCode class BlogTypeAndAuthor { BlogPostType type; String author; // constructor, getter, setter method }
Nhóm theo BlogTypeAndAuthor class.
Map<BlogTypeAndAuthor, List<BlogPost>> postsPerTypeAndAuthor = posts.stream() .collect(groupingBy(post -> new BlogTypeAndAuthor(post.getType(), post.getAuthor())));
Modify Type Value trong Map
Method overload thứ 2 của GroupingBy
static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
Nhận thêm tham số thứ 2 downstream sẽ được dùng để áp dụng cho việc lấy kết quả và xác định kiểu dữ liệu của value trong Map. Nếu downstream không được truyền, mặc định Collectors sẽ sử dụng toList() method
public static <T> Collector<T, ?, List<T>> toList() { return new Collectors.CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }, CH_ID); }
Chúng ta có thể sử dụng toSet() được xây dựng sẵn trong Collectors để nhận một Set<BlogPost> thay vì List<BlogPost>.
Map<BlogPostType, Set<BlogPost>> postsPerType = posts.stream() .collect(groupingBy(BlogPost::getType, toSet()));
Group by Multiple Fields
Ví dụ chúng ta muốn nhóm theo Author, với những BlogPost cùng Author lại nhóm theo BlogType.
Map<String, Map<BlogPostType, List>> map = posts.stream() .collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));
Average from Grouped Results
Vẫn sử dụng
static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
Chúng ta có thể kết hợp các averaging() method để tính giá trị trung bình của value thay vì nhận lại một List hay Set value.
Map<BlogPostType, Double> averageLikesPerType = posts.stream() .collect(groupingBy(BlogPost::getType, averagingInt(BlogPost::getLikes)));
Sum from Grouped Results
Tương tự, Collectors class cũng cung cấp cho chúng ta có sum() method để tính tổng giá trị của value.
Map<BlogPostType, Integer> likesPerType = posts.stream() .collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes)));
Max & Min from Grouped Results
Sử dụng maxBy(), minBy() method để lấy value có giá trị lớn nhất theo thuộc tính được chỉ định.
Ví dụ nhóm BlogPost theo BlogType và chỉ lấy BlogPost có lượt like nhiều nhất.
Map<BlogPostType, Optional<BlogPost>> maxLikesPerPostType = posts.stream() .collect(groupingBy(BlogPost::getType, maxBy(comparingInt(BlogPost::getLikes))));
Tương tự, chúng ta có áp dụng với minBy(). Các bạn chú ý rằng maxBy() và minBy() trả về Optional value để tránh trường hợp collection value rỗng.
Summary for an Attribute of Grouped Results
Nảy giờ chúng ta đã tìm hiểu được cách value nhỏ nhất, lớn nhất, trung bình, tuy nhiên nếu nhu cầu của chúng ta cần sử dụng 3 chức năng này một lúc thì sao? không lẽ code 3 đoạn code dài ngằn!
Không nha, sử dụng summarizing() method cho phép chúng ta gom nhóm các giá trị nhỏ nhất, lớn nhất, trung bình vào cùng một object theo một thuộc tính cụ thể.
Ví dụ summarizing() cho Like
Map<BlogPostType, IntSummaryStatistics> likeStatisticsPerType = posts.stream() .collect(groupingBy(BlogPost::getType, summarizingInt(BlogPost::getLikes)));
IntSummaryStatistics object chứa method getSum(), getMax(), getMin() ,getAverage() trả về tổng like của một BlogType, hay BlogPost có số like nhiều nhất trong BlogType etc.
Modify Map Type
Khi sử dụng groupingBy(), mặc định nó sẽ trả về HashMap instance, nếu muốn thay đổi kiểu dữ liệu mặc định này, chúng ta sẽ phải sử dụng đến method overload thứ 3 của groupingBy().
Ví dụ nhận kết quả trả về là một EnumMap bằng cách cung cấp một Supplier function trả về EnumMap.
EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream() .collect(groupingBy(BlogPost::getType, () -> new EnumMap<>(BlogPostType.class), toList()));
Tóm lược
Như vậy chúng ta đã tìm hiểu được cách sử dụng của groupingBy() method của Collectors class. Đây thật là một công cụ mạnh mẽ để thao tác với Stream API hiệu qủa hơn trong các hoạt động gom nhóm đối tượng.
Nguồn tham khảo