Java 8 - 自訂收集器(Custom Collectors)

之前在撰寫相關文章時Java 8 - 利用Lambda與Stream來改寫Collection的基本操作在處理Stream的最後皆可能使用collect method並且帶入Collectors.toLists() or Collectors.toSet()來將Stream內的資料流轉成List or Set的形式!  不過若是我們要自訂收集器的話也是可行的,如此一來可以自行定義複雜的收集器將相關相關method重新實作!

需求如下:
根據上面Java 8相關文章有提到Transcript class每個學生都有自己的三科成績,請依據條件幫我求得每位符合最多的學生有誰?  也就是指定一個分數90,若某位學生有最多個科目皆比這分數還高,最多的人就滿足條件!   要自訂收集器請先implements Collector,由於Collector為泛型格式,因此需指定三個資料型態給它 -  Interface Collector<T,A,R>

官方說明如下:
Type Parameters:
T - the type of input elements to the reduction operation
A - the mutable accumulation type of the reduction operation (often hidden as an implementation detail)
R - the result type of the reduction operation

第一個參數為stream的資料型態
第二個參數為建立一內部收集容器
第三個參數為呼叫toCollect()後回傳的資料格式
透過IDE可以自動產生相關需要實作的method
class ExcellentMenu implements Collector<Transcript, Map<Transcript, Integer>, List<String>> {
 int standard;
 
 public ExcellentMenu(int standard){
    this.standard = standard;
 }
 
 @Override
 public Supplier<Map<Transcript, Integer>> supplier() {
  // TODO Auto-generated method stub
  return null;
 }

 @Override
 public BiConsumer<Map<Transcript, Integer>, Transcript> accumulator() {
  // TODO Auto-generated method stub
  return null;
 }

 @Override
 public BinaryOperator<Map<Transcript, Integer>> combiner() {
  // TODO Auto-generated method stub
  return null;
 }

 @Override
 public Function<Map<Transcript, Integer>, List<String>> finisher() {
  // TODO Auto-generated method stub
  return null;
 }

 @Override
 public Set<java.util.stream.Collector.Characteristics> characteristics() {
  // TODO Auto-generated method stub
  return null;
 }
}

在此說明重新實作Collector method,基本上這些method的回傳型態都是屬於
java.util.function(除了characteristics),因此回傳的資料格式可以使用lambda expression

官方表示如下:
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.

而官方有提供Collector內的呼叫流程範例如下:
A a2 = supplier.get();
accumulator.accept(a2, t1);
A a3 = supplier.get();
accumulator.accept(a3, t2);
R r2 = finisher.apply(combiner.apply(a2, a3));  // result with splitting

如此一來,可以了解到這些method該如何利用lambda expression來實作以supplier為例,functional interface's get method本身implements不需assign任一參數且回傳的型態為A,在此即為收集容器本身!

1. supplier
@Override
public Supplier<Map<Transcript, Integer>> supplier() {
 // TODO Auto-generated method stub
 return () -> new HashMap<>();
}

實作如何定義一收集容器的實體!(在此為Map<Transcript, Integer>)

2. accumulator
@Override
public BiConsumer<Map<Transcript, Integer>, Transcript> accumulator() {
 // TODO Auto-generated method stub
 return (acc, elem) -> {
  int i = 0;
  if(elem.getChinese() >= standard)i++;
  if(elem.getEnglish() >= standard)i++;
  if(elem.getMath() >= standard)i++;
  acc.put(elem, i);
 };
}

實作如何使用收集容器(A)及stream資料型態(T),並進行迭代處理將T存入A內在此判斷這三個科目是否有滿足所設定的條件,將計數的結果put到Map內
PS. standard為一內部自行定義的成員變數,於初始化collectors時帶入實際值

3. combiner
@Override
public BinaryOperator<Map<Transcript, Integer>> combiner() {
 // TODO Auto-generated method stub
 return (acc1, acc2) -> {
  acc1.putAll(acc2);
  return acc1;
 };
}

實作如何合併內部收集器
特別地是,收集器可以使用sequential and parallel executions produce equivalent results
在此我們使用的stream非parallel,因此這個combiner的method不會使用到!!
PS. 從這篇文章中有特別說明到

4. finisher
@Override
public Function<Map<Transcript, Integer>, List<String>> finisher() {
 // TODO Auto-generated method stub
 return (accmulator) -> {
  int max = accmulator.entrySet().stream()
      .reduce((elem1, elem2) -> elem1.getValue() > elem2.getValue() ? elem1 : elem2)
      .map(entry -> entry.getValue()).get();
  return accmulator.entrySet().stream()
    .filter(n -> n.getValue() == max)
    .map(entry -> entry.getKey().getName())
    .collect(Collectors.toList());
 };
}

實作如何定義呼叫collect所回傳的內容
在此所定義的行為是先求得max變數值,表示所有的Transcript所對應的Integer最大值為多少這個最大值表示滿足>=standard的個數
最後,根據這max個數得到有哪些Transcript滿足這個條件,並回傳相關名單!

5. characteristics
@Override
public Set<java.util.stream.Collector.Characteristics> characteristics() {
 // TODO Auto-generated method stub
 return Collections.emptySet();
}

最後要定義的method characteristics,由這篇文章可以得知我們只要回傳empty Set即可!

最後就是主程式部分
List<Transcript> studentList = new ArrayList<Transcript>();
Transcript t1 = new Transcript("Ben", 87, 91, 80);
Transcript t2 = new Transcript("Sjkok", 94, 99, 90);
Transcript t3 = new Transcript("Aaron", 44, 55, 100);
Transcript t4 = new Transcript("Zhibin", 77, 88, 66);
studentList.add(t1);
studentList.add(t2);
studentList.add(t3);
studentList.add(t4);
  
System.out.println(studentList.stream().collect(new ExcellentMenu(80)));

在這邊輸出的結果為[Ben, Sjkok],表示這兩位同學的成績單各科大於等於80分為數目最多的人。


最後,在此實作了這些相關的functional interface所宣告的method,讓我了解到lambda與此之間的關係,在此應找時間深入多了解functional programming才是!
Reference
1. http://www.codedata.com.tw/java/jdk8-functional-api/
2. http://blog.radoszewski.pl/programming/java/2015/07/31/custom-java-8-collectors.html

留言