[Java] Custom collector trong Java 8

3464

Trong số các tính năng mới xuất hiện của Java 8, Stream được xem như một yếu tố tác động mạnh mẽ tới việc viết code của lập trình viên Java.

Quá trình sử dụng Stream mang tính tuyến tính: stream được tạo ra từ một collection, nó được xử lý bởi một hoặc nhiều stream method, sau đó nó được thu hồi về phía collection hoặc object.. Tại bước này, ta có thể xuất thành các kiểu collection sau:

  • Collectors.toList()
  • Collectos.toSet()
  • Collectors.toMap()

……

Và vấn đề phát sinh ở đây là làm thế nào xuất từ Stream ra một kiểu collection hoàn toàn khác, không có trong API của Collectors? Bài viết này sẽ giúp bạn làm điều đó.

#1 Interface Collector

Mỗi static method liệt kê ở trên đều trả về một object kiểu Collector. Nhưng Collector là gì? Bạn hãy xem biểu đồ sau:

Ta thấy có 4 interface liên quan:

Supplier: biểu diễn supplier của một kết quả. Mỗi khi supplier được invoke, không có ràng buộc về tính chất của result (result mới hoặc trùng nhau)

BiConsumer: biểu diễn một operation có 2 biến đầu vào nhưng đầu ra không có gì.

Function: Biểu diễn một hàm có 1 biến đầu vào và đầu ra là 1 kết quả nào đó

BinaryOperator: biểu diễn một operation có đầu vào là 2 toán tử cùng loại (same type), kết quả cũng cùng type như toán tử. Đây là một trường hợp của BiFunction khi mà toán tử và kết quả có cùng kiểu (type).

Trích dẫn từ phần documentation của Collector, ta có:

Một Collector được định nghĩa từ  4 function làm việc cùng nhau để dồn các entries thành một result container (container này có đặc tính mutable), bên cạnh đó nó có thể thực hiện bước final transform cho result (nếu được yêu cầu). 4 functions đó gồm:

supplier() – có vai trò khởi tạo result container

accumulator() – đẩy các phần tử dữ liệu mới vào result container

combiner() – kết hợp 2 result container thành 1

finisher() – thực thi final transform cho container (nếu được yêu cầu)

#2 Stream.collect()

Phần doc của Stream.collect() hé lộ khá nhiều điều:

Method này thực hiện một phép toán tên là mutable reduction (mutable reduction operation). Đây là phép toán làm giảm giá trị của một mutable result container (Ví dụ: ArrayList), và các thành phần trong result container đó cũng bị tác động bới việc bị giảm đi một giá trị tương ứng. Phép toán này tương đương:

combiner() sẽ không được sử dụng cho đến khi ta làm việc với parallel stream

#3 Các ví dụ

Single-value example:

Để khởi động, hãy tính toán kích thước của một collection sử dụng Collector. Mặc dù cách này không thực sự hiệu quả và được sử dụng rộng rãi nhưng nó là một ví dụ dễ làm quen.

Sau đây là các yêu cầu của 4 interface:

1. Nếu kết quả cuối cùng (end result) là integer thì supplier cũng phải trả về integer. Tuy nhiên int hay Integer đều có tính Immutable, do đó ta cần đến MutableInt từ thư viện Apache Common Lang.

2. Bộ gộp (accumulator) chỉ nên thay đổi giá trị của các phần tử mang kiểu MutableInt thuộc result container (cụ thể trong ví dụ này là gọi hàm increment() )

3. Giá trị trả về là int được wrap trong MutableInt

Hãy xem qua 4 class của ví dụ này:

MutableIntSupplier.java

MutablieIntObjectAccumulator.java

MutableIntCombiner.java

MutableIntFinisher.java

SizeCollector.java

Grouping example:

Ví dụ thứ hai liên quan tới collection chứa string. Ta sẽ tạo một multi-valued Map với 2 tiêu chí:

  • Phần Key có kiểu dữ liệu là char
  • Phần Values tương ứng là String có kí tự đầu tiên là Key.

Các yêu cầu dành cho 4 inteface:

1. Supplier trả về một bản thể kiểu MultivaluedMap

2. Accumulator sẽ gọi put() từ multi-valued map, sử dụng các mô tả đi kèm với bản thể MultivaluedMap được trả về bởi supplier

3. finisher sẽ trả về map

Đây là source code minh họa:

MultiValuedMapSupplier.java

MultiValuedMapAccumulator.java

MultiValuedMapCombiner.java

GroupbyFirstCharacterCollector.java

Partritioning example
Ví dụ thứ 3 diễn tả lại một use-case mà tác giả gặp phải: cho một collection và các thành phần sẽ chuẩn bị được thêm vào collection, tách riêng các thành phần này thành 2 nhóm: 1 nhóm có thể được add vào Collection này, và nhóm còn lại thì không thể add vào.

Yêu cầu cho 4 interface:

1. Supplier sẽ trả về một bản thể mang kiểu của một data structure phù hợp (trong trường hợp này, hãy xem xét kiểu DoubleList)

2. Accumulator cần được init với các thành phần chuẩn bị được thêm vào collection,

3. Finisher cần phải trả về 1 bản thể của DoubleList.

DoubleListSupplier.java

DoubleListAccumulator.java

DoubleListCombiner.java

DoubeList.java

 

Techtalk via N.Frankel’s blog

 

CHIA SẺ