Java 8-Stream API-用流收集資料
用指令使風格對交易按照年份分組
@Test
public void test9() {
//建立根據年份分組的Map
Map<Integer,List<Transaction>> transactionByCurrencies=new HashMap<>();
//遍歷Transaction的List
for (Transaction transaction : transactions) {
//提取Transaction的年份
Integer year = transaction.getYear();
List<Transaction> transactionsForCurrency= transactionByCurrencies.get(year);
//如果分組Map中沒有這個年份,就建立一個
if(transactionsForCurrency==null){
transactionsForCurrency=new ArrayList<>();
transactionByCurrencies.put(year,transactionsForCurrency);
}
//將當前遍歷的Transaction加入同一年份的Transaction的List
transactionsForCurrency.add(transaction);
}
System.out.println(transactionByCurrencies);
}
用流收集資料
Map<Integer,List<Transaction>> transactionByCurrencies=
transactions.stream().collect(groupingBy(Transaction::getCurrency));
收集器簡介
函數語言程式設計相對於指令式程式設計的一個主要優勢:你只需指出希望的結果—做什麼,而不用操心執行的步驟—如何做。
groupingBy說的是“生成一個Map,它的鍵是(貨幣)桶”,值則是桶中那些元素的列表。
優秀的函式式API設計的另一個好處:更易複合和重用。收集器非常有用,因為用它可以簡潔而靈活地定義collect用來生成結果集合的標準。更具體地說,對流呼叫collect方法將對流中的元素觸發一個歸約操作(由Collector來引數化)
一般來說,Collector會對元素應用一個轉換函式(很多時候是不體現任何效果的恆等轉換,例如toList),並將結果積累在一個數據結構中,從而產生這一過程的最終輸出。例如,在前面所示的交易分組的例子中,轉換函式提取了每筆交易的時間,隨後使用貨幣作為鍵,將交易本身累積在生成的Map中。
Collector介面中方法的實現決定了如何對流執行歸約操作。我們可以建立自定義收集器。但Collectors實用類提供了很多靜態工廠方法,可以方便地建立常見收集器的例項,只要拿來用就可以了。最直接和最常用的收集器是toList靜態方法,它會把流中所有的元素收集到一個List中。
預定義的收集器
從Collectors類提供的工廠方法(例如groupingBy)建立的收集器。它們主要提供了三大功能:
- 將流元素歸約和彙總為一個值
- 元素分組
- 元素分割槽
歸約和彙總
利用counting工廠方法返回的收集器,數一數選單裡有多少種菜
long howManyDishes=menu.stream().collect(Collectors.counting());
這還可以寫得更為直接:
long howManyDishes=menu.stream().count();
counting收集器在和其他收集器聯合使用的時候特別有用。
在後面的部分,我們假定你已匯入了Collectors類的所有靜態工廠方法:
import static java.util.stream.Collectors.*;
這樣你就可以寫counting()而用不著寫Collectors.counting()之類的了。
查詢流中的最大值和最小值
找出選單中熱量最高的菜。Collectors.maxBy和Collectors.minBy。這兩個收集器接收一個Comparator引數來比較流中的元素。
Comparator<Dish> dishColoriesComparator=
Comparator.comparing(Dish::getCalories);
Optional<Dish> mostCalorieDish=
menu.stream()
.collect(maxBy(dishCaloriesComparator));
彙總
Collectors.summingInt。它可接受一個把物件對映為求和所需int的函式,並返回一個收集器;該收集器在傳遞給普通的collect方法後即執行我們需要的彙總操作。可以這樣求出選單列表的總熱量:
int totalCalories=menu.stream().collect(summingInt(Dish::getCalories));
在遍歷流時,會把每一道菜都對映為其熱量,然後把這個數字累加到一個累加器(這裡的初始值為0)
Collectors.summingLong和Collectors.summingDouble方法的作用完全一樣,可以用於求和欄位為long或double的情況。
但彙總不僅僅是求和;還有Collectors.averagingInt,連同對應的averagingLong和averagingDouble可以計算數值的平均數
double avgCalories=
menu.stream().collect(averagingInt(Dish::getCalories));
有時候,你可能想要得到兩個或更多這樣的結果,而且你希望只需一次操作就可以完成。在這種情況下,你可以使用summarizingInt工廠方法返回的收集器。例如,通過一次summarizing操作你可以就數出選單中元素的個數,並得到菜餚熱量總和、平均值、最大值和最小值。
IntSummaryStatistics menuStatistics=menu.stream().collect(summarizingInt(Dish::getColories));
System.out.println(menuStatistics.getCount());
同樣,相應的summarizingLong和summarizingDouble工廠方法有相關的LongSummaryStatistics和DoubleSummaryStatistics型別,適用於收集的屬性是原始型別long或double的情況。
連線字串
joining工廠方法返回的收集器會把對流中每一個物件應用toString方法得到的所有字元連線成一個字串。
String shortMenu=menu.stream().map(Dish::getName).collect(joining());
joining在內部使用了StringBuilder來把生成的字串逐個追加起來。
joining工廠方法有一個過載版本可以接受元素之間的分界符。
String shortMenu=menu.stream().map(Dish::getName).collect(joining(","));
廣義的歸約彙總
我們已經討論的所有收集器,都是一個可以用reducing工廠方法定義的歸約過程的特殊情況而已。Collectors.reducing工廠方法是所有這些特殊情況的一般化。
可以用reducing方法建立的收集器來計算你選單的總熱量
int totalCalories=menu.stream().collect(reducing(0,Dish::getCalories,(i,j)->i+j));
它需要三個引數:
- 第一個引數是歸約操作的起始值,也是流中沒有元素時的返回值
- 第二個引數是
- 第三個引數是一個BinaryOperator,
可以使用單引數形式的reducing來找到熱量最高的菜
Optional<Dish> mostCalorieDish=
menu.stream().collect(reducing(
(d1,d2)->d1.getCalories()>d2.getCalories()?d1:d2));
可以把單引數reducing工廠方法建立的收集器看作三引數方法的特殊情況,它把流中的第一個專案作為起點,把恆等函式(即第一個函式僅僅是返回其輸入引數)作為一個轉換函式。這也意味著,要是把單引數reducing收集器傳遞給空流的collect方法,收集器就沒有起點。
reducing方法有三個過載函式
收集與歸約
Stream介面的collect和reduce方法有何不同,因為兩種方法通常會獲得相同的結果。
可以像下面這樣使用reduce方法來實現toListCollector所作的工作:
Stream<Integer> stream=Arrays.asList(1,2,3,4,5,6).stream();
List<Integer> numbers=stream.reduce(
new ArrayList<>(),
(List<Integer> l, Integer e)->{
l.add(e);
return l;
},
(List<Integer> l1,List<Integer> l2)->{
l1.addAll(l2);
return l1;
});
這個解決方案有兩個問題:一個語義問題和一個實際問題。語義問題在於,reduce方法旨在把兩個值結合起來生成一個新值,它是一個不可變的歸約。與此相反,collect方法的設計就是要改變容器,從而積累要輸出的結果。這意味著,上面的程式碼片段是在濫用reduce方法,因為它在原地改變了作為累加器的List。
以錯誤的語義使用reduce方法還會造成一個實際問題:這個歸約過程不能並行工作,因為由多個執行緒併發修改同一個資料結構可能會破壞List本身。在這種情況下,如果你想要執行緒安全,就需要每次分配一個新的List,而物件分配又會影響效能。這就是collect方法特別適合表達可變容器上的歸約的原因,更關鍵的是它適合並行操作。
函數語言程式設計提供了多種方法來執行同一個操作。收集器在某種程度上比Stream介面上直接提供的方法用起來更復雜,但好處在於它們能提供更高水平的抽象和概括,可更容易重用和自定義。
建議,儘可能為手頭的問題探索不同的解決方案,但在通用的方案裡面,始終選擇最專門化的一個。
分組
用Collectors.groupingBy工廠方法返回的收集器就可以輕鬆地完成這項任務。
Map<Dish.Type,List<Dish>> dishesByType=menu.stream().collect(groupingBy(Dish::getType));
其結果是下面的Map
{OTHER=[french fries, rice, season fruit, pizza], FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}
給groupingBy方法傳遞了一個Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。把這個Function叫作分組函式,因為它用來把流中的元素分成不同的組。
分組操作的結果是一個Map,把分組函式返回的值作為對映的鍵,把流中所有具有這個分類值的專案的列表作為對應的對映值。在選單分類的例子中,鍵就是菜的型別,值就是包含所有對應型別的菜餚的列表。
但是,分類函式不一定像方法引用那樣可用,因為你想用以分類的條件可能比簡單的屬性訪問器要複雜。例如,你可能想把熱量不到400卡路里的菜劃分為“低熱量”(diet),熱量400到700卡路里的菜劃分為“普通”(normal),高於700卡路里的劃為“高熱量”(fat)。
由於Dish類的作者沒有把這個操作寫成一個方法,你無法使用方法引用,但你可以把這個邏輯寫成Lambda表示式:
public enum CaloricLevel{DIET,NORMAL,FAT}
Map<CaloricLevel,List<Dish>> dishesByCaloricLevel=menu.stream().collect(
groupingBy(dish->{
if (dish.getColories()<=400) return CaloricLevel.DIET;
else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}));
多級分組
要實現多級分組,我們可以使用一個由雙引數版本的Collectors.groupingBy工廠方法建立的收集器,它除了普通的分類函式之外,還可以接受collector型別的第二個引數。那麼要進行二級分組的話,我們可以把一個內層groupingBy傳遞給外層groupingBy,並定義一個為流中專案分類的二級標準。
Map<Dish.Type,Map<CaloricLevel,List<Dish>>> dishesByTypeCaloricLevel=menu.stream().collect(
groupingBy(Dish::getType,
groupingBy(dish->{
if (dish.getColories()<=400) return CaloricLevel.DIET;
else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}))
);
}
二級分組的結果就是像下面這樣的兩極Map:
{OTHER={DIET=[rice], FAT=[season fruit], NORMAL=[french fries, pizza]}, FISH={DIET=[prawns], NORMAL=[salmon]}, MEAT={DIET=[chicken], FAT=[pork], NORMAL=[beef]}}
這裡的外層Map的鍵就是第一級分類函式生成的值“fish,meat,other”,而這個Map的值又是一個Map,鍵是二級分類函式生成的值“normal,diet,fat”。
這種多級分組操作可以擴充套件至任意層級,n級分組就會得到一個代表n級樹形結果的n級Map
把groupingBy看作“桶”比較容易明白。第一個groupingBy給每個鍵建立了一個桶。然後再利用下游的收集器去收集每個桶中的元素,以此得到n級分組。
按子組收集資料
可以把第二個groupingBy收集器傳遞給外層收集器來實現多級分組。但進一步說,傳遞給第一個groupingBy的第二個收集器可以是任何型別,而不一定是另一個groupingBy。例如,要數一數選單中每類菜有多少個,可以傳遞counting收集器作為groupingBy收集器的第二個引數。
Map<Dish.Type,Long> typesCount=menu.stream().collect(
groupingBy(Dish::getType,counting()));
普通的單引數groupingBy(f)實際上是groupingBy(f,toList())的簡便寫法
查詢每類菜中熱量最高的菜餚
Map<Dish.Type,Optional<Dish>> mostCaloricByType=
menu.stream()
.collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getColories))));
這個Map中的值是Optional,因為這是maxBy工廠方法生成的收集器的型別,但實際上,如果選單中沒有某一型別的Dish,這個型別就不會對應一個Optional.empty()值,而且根本不會出現在Map的鍵中。groupingBy收集器只有在應用分組條件後,第一次在流中找到某個鍵對應的元素時才會把鍵加入分組Map中。這意味Optional包裝器在這裡不是很有用,因為它不會僅僅因為它是歸約收集器的返回型別而表達一個最終可能不存在卻意外存在的值。
1.把收集器的結果轉換為另一種型別
因為分組操作的Map結果中每個值上包裝的Optional沒什麼用,所以你可能想要把它們去掉。把收集器返回的結果轉換為另一種型別,你可以使用Collectors.collectingAndThen工廠方法返回的收集器。
Map<Dish.Type,Dish> mostCaloricByType=menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getColories)),Optional::get)));
這個工廠方法接受兩個引數—要轉換的收集器以及轉換函式,並返回另一個收集器。
2.與groupingBy聯合使用的其他收集器的例子
求每種型別菜餚熱量的總和
Map<Dish.Type,Integer> totalCaloriesByType=
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
然而常常和groupingBy聯合使用的另一個收集器是mapping方法生成的。這個方法接受兩個引數:一個函式對流中的元素做變換,另一個則將變換的結果物件收集起來。其目的是在累加之前對每個輸入元素應用一個對映函式,這樣就可以讓接受特定型別元素的收集器適應不同型別的物件。
需求:對於每種型別的Dish,選單中都有哪些CaloricLevel。
Map<Dish.Type,Set<CaloricLevel>> caloricLevelsByType= menu.stream().collect(
groupingBy(Dish::getType,mapping(
dish->{
if (dish.getColories()<=400) return CaloricLevel.DIET;
else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
},toSet()))
);
上面對於返回的Set是什麼型別並沒有任何保證。但通過使用toCollection,你就可以有更多的控制。例如,你可以給它傳遞一個建構函式引用來要求HashSet:
Map<Dish.Type,Set<CaloricLevel>> caloricLevelsByType= menu.stream().collect(
groupingBy(Dish::getType,mapping(
dish->{
if (dish.getColories()<=400) return CaloricLevel.DIET;
else if(dish.getColories()<=700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
},toCollection(HashSet::new)))
);
分割槽
分割槽是分組的特殊情況:由一個謂詞(返回一個布林值的函式)作為分類函式,它稱為分割槽函式。
分割槽函式返回一個布林值,這意味著得到的分組Map的鍵型別是Boolean,於是它最多可以分為兩組—true是一組,false是一組。例如,如果你是素食者或是請了一位素食的朋友來共進晚餐,可能會想要把選單按照素食和非素食分開。
Map<Boolean,List<Dish>> partitionedMenu=menu.stream().collect(
partitioningBy(Dish::isVegetarian)
);
{false=[pork, beef, chicken, season fruit, prawns, salmon], true=[french fries, rice, pizza]}
用同樣的分割槽謂詞,對選單List建立的流作篩選,然後把結果收集到另外一個List中也可以獲得相同的結果:
List<Dish> vegetarianDishes=menu.stream().filter(Dish::isVegetarian).collect(toList());
分割槽的優勢
分割槽的好處在於保留了分割槽函式返回true或false的兩套元素列表。
partitioningBy工廠方法有一個過載版本,可以像下面這樣傳遞第二個收集器:
Map<Boolean,Map<Dish.Type,List<Dish>>> vegetarianDishesByType=menu.stream().collect(
partitioningBy(Dish::isVegetarian,groupingBy(Dish::getType)));
{false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon], OTHER=[season fruit]}, true={OTHER=[french fries, rice, pizza]}}
工廠方法 | 返回型別 | 用於 |
---|---|---|
toList | List<T> |
把流中所有專案收集到一個List |
toSet | Set<T> |
把流中所有專案收集的一個Set,刪除重複項 |
toCollection | Collectiion<T> |
把流中所有專案收集到給定的供應源建立的集合 |
counting | Long | 計算流中元素的個數 |
summingInt | Integer | 對流中專案的一個整數屬性求和 |
averagingInt | Double | 計算流中專案Integer屬性的平均值 |
summarizingInt | IntSummaryStatistics | 收集關於流中專案Integer屬性的統計值,例如最大、最小、總和與平均值 |
joining | String | 連線對流中每個專案呼叫toString方法所生成的字串 |
maxBy | Optional<T> |
一個包裹了流中按照給定比較器選出的最大元素的Optional,或如果流為空則為Optional.empty() |
minBy | Optional<T> |
一個包裹了流中按照給定比較器選出的最小元素的Optional,或如果流為空則為Optional.empty() |
reducing | 歸約操作產生的型別 | 從一個作為累加器的初始值開始,利用BinaryOperator與流中元素逐個結合,從而將流歸約為單個值 |
collectingAndThen | 轉換函式返回的型別 | 包裹另一個收集器,對其結果應用轉換函式 |
groupingBy | Map<K,List<T>> |
根據專案的一個屬性的值對流中的專案作為組,並將屬性值作為結果Map的鍵 |
partitioningBy | Map<Boolean,List<T>> |
根據對流中每個專案應用謂詞的結果來對專案進行分割槽 |
小結
- collect是一個終端操作,它接受的引數是將流中元素累積到彙總結果的各種方式(稱為收集器)
- 預定義收集器包括將流元素歸約和彙總到一個值,例如計算最小值、最大值或平均值
- 預定義收集器可以用groupingBy對流中元素進行分組,或用partitioningBy進行分割槽
- 收集器可以高效地複合起來,進行多級分組、分割槽和歸約
- 可以實現Collector介面中定義的方法來開發你自己的收集器