從Stream類中的collect作爲本文的開始。現在有一個裝有Frog對象的List集合,從這個list中將frog對象的size值提取成一個新的集合,可以通過如下方法實現:
List<Double> sizeList = frogList.stream()
.map(Frog::getSize)
.collect(Collectors.toList());
Forg類
public class Frog {
private String name;
private Integer age;
private String color;
private Double size;
// 省略getter和setter等方法
}
上面代碼大致意思爲:stream方法將集合轉化爲流,map方法將frog對象的size值提取出來,collect用來將size的值再收集起來。前面兩個方法都比較好理解,重點關注collect。collect方法的參數是一個Collector接口,表示接收一個收集器(一個收集的行爲或者說按什麼規則收集流中數據),按照這個行爲方式將流中數據收集起來(行爲參數化)。問題是這個行爲該怎麼定義,收集器執行流程是怎樣?(一定要看到文末)
收集器僞流程圖
圖中c框中就是收集器要做的事情,分別對應Collector接口中幾個方法:
1.supplier 構建容器
2.accumulator 收集元素
3.combiner 合併結果
4.finisher 轉化爲最終值
Collector接口
在分析Collector接口之前先看一下java8幾個常用的函數式接口:
接口 | 參數 | 返回類型 | 說明 |
---|---|---|---|
Predicate | T | boolean | 輸入某個值,輸出boolean值,用於對某值進行判定 |
Consumer | T | void | 輸入某值,無輸出。用於消費某值 |
Function<T,R> | T | R | 輸入某類型值,輸出另種類型值,用於類型轉化等 |
Supplier | T | 無輸入,輸出某值,用於生產某值 | |
UnaryOperator | T | T | 輸入某類型值,輸出同類型值,用於值的同類型轉化,如對值進行四則運算等 |
BinaryOperator | (T,T) | T | 輸入兩個某類型值,輸出一個同類型值,用於兩值合併等 |
Collector接口的泛型爲Collector<T, A, R>,其中第一個泛型T表示輸入的類型,第二個A表示累計時的類型,第三個R表示最終的結果類型。
下面看一下接口中的幾個方法:
supplier (容器)
該方法需要返回一個Supplier<A>接口,Supplier不需要輸入,用於輸出值,類似一個工廠,用來構建收集數據的容器。
accumulator (累加器)
該方法需要返回一個BiConsumer<A, T>接口,與表中Consumer功能一樣,表示消費類型爲A和T的值,在這裏應該讓輸入的T類型的值累積到A類型的容器中。
combiner (合併)
該方法需要返回一個BinaryOperator<A>接口,這裏表示將多個容器合併爲一個容器。
finisher (修整)
該方法需要返回一個Function<A, R>接口,完成中間值到最終值得轉換。
characteristics (特徵)
該方法表示收集器應該有的特徵,枚舉中分別表示爲CONCURRENT(併發)、UNORDERED(無序)、IDENTITY_FINISH(一致性完成,恆等函數功能)
toList()的實現
解釋這麼多還是難以理解,所以還是找個例子看看該如何構建一個收集器。
Collectors的內部類CollectorImpl實現了Collector接口,而Collectors的靜態方法toList又使用了CollectorImpl的構造方法返回一個Collector,下面就通過toList來分析收集器如何誕生(其實應該理解爲收集流中數據的這種行爲如何定義或者是收集流中數據的步驟如何制定)。
1 首先構建實際類型爲ArrayList的集合(前面所說的容器),用來存放流中元素。結合文章開頭此時輸入的元素是Frog的的size屬性值,類型爲Double,即泛型T應該爲Double類型。
2 通過add方法將元素收集到List集合中,完成累加功能。
3 List中addAll方法是一個集合將另外一個集合中所有元素添加到自己內部,在這裏代表完成了容器的合併。
4 指定收集特徵爲IDENTITY_FINISH,一致性完成,如恆等函數,輸入即輸出不做任何處理,那麼輸出爲List<Double>類型。
5 中間少了修整這一步,默認爲castingIdentity方法,即無修整。
自定義收集器
通過分析toList實現邏輯發現之前那些概念清晰了很多,可以着手實現一個自定義的收集器了。不需要在添加一個Collector接口的實現類,使用接口中的of方法可以便捷的返回一個Collector。
同時將業務場景升級一下,假設要得到一個這樣的集合,將frogList先按name分組,分組後需要將每組的size收集成一個字符串集合,即需要一個<Map<String,List<String>>類型。
DecimalFormat df = new DecimalFormat("0.000000"); // 格式化
Map<String, ArrayList<Object>> frogMap = frogList.stream()
.collect(Collectors.groupingBy(
Frog::getName,
Collector.of(
ArrayList::new,
(list, frog) -> list.add(df.format(frog.getSize())),
(listA, listB) -> {
listA.addAll(listB);
return listA;
},
Collector.Characteristics.IDENTITY_FINISH
)));
這個自定義的收集器其實只是在double轉string格式化的時候做了一個處理,還有一個更方便的處理方式:
DecimalFormat df = new DecimalFormat("0.000000");
Map<String, List<String>> collect = frogList.stream()
.collect(Collectors.groupingBy(
Frog::getName,
Collectors.mapping(
frog -> df.format(frog.getSize()),
Collectors.toList()))
);
相比之下後一種清晰了許多。所以除非特殊情況出現,大多數情況還是用不到自定義收集器的,只要善於挖掘這些已經有的方法就好。
(END)