Java 8 - 收集器Collectors_分组groupingBy
文章目錄
- Pre
- 多級分組
- 按子組收集數(shù)據(jù)
- 查找每個子組中熱量最高的 Dish
- 圖解工作過程
- 與 groupingBy聯(lián)合使用的其他收集器的例子
- 附
Pre
來看個小例子: 把菜單中的菜按照類型進行分類,有菜的放一組,有肉的放一組,其他的都放另一組。
Map<Dish.Type, List<Dish>> collect = menu.stream().collect(groupingBy(Dish::getType));用 Collectors.groupingBy 工廠方法返回的收集器就可以輕松地完成這項任務。
這里,給 groupingBy 方法傳遞了一個 Function (以方法引用的形式),它提取了流中每一道 Dish 的 Dish.Type 。我們把這個 Function 叫作分類函數(shù),因為它用來把流中的元素分成不同的組。
如下圖所示,分組操作的結(jié)果是一個 Map ,把分組函數(shù)返回的值作為映射的鍵,把流中所有具有這個分類值的項目的列表作為對應的映射值。
在菜單分類的例子中,鍵就是菜的類型,值就是包含所有對應類型的菜的列表。
【第二個例子】
但是,分類函數(shù)不一定像方法引用那樣可用,因為你想用以分類的條件可能比簡單的屬性訪問器要復雜。
例如,你可能想把熱量不到400卡路里的菜分為“低熱量”(diet),熱量400到700卡路里的菜為“普通”(normal),高于700卡路里的菜為“高熱量”(fat)。
由于 Dish 類 沒有把這個操作寫成一個方法,你無法使用方法引用,但你可以把這個邏輯寫成Lambda表達式:
public enum CaloricLevel { DIET, NORMAL, FAT }Map<CaloricLevel, List<Dish>> collect = menu.stream().collect(groupingBy(dish -> {if (dish.getCalories() > 300) {return CaloricLevel.DIET;} else if (dish.getCalories() <= 700) {return CaloricLevel.NORMAL;} else {return CaloricLevel.FAT;}}));多級分組
現(xiàn)在,已經(jīng)看到了如何對菜單中的菜肴按照類型和熱量進行分組,但要是想同時按照這兩個標準分類怎么辦呢?分組的強大之處就在于它可以有效地組合。讓我們來看看怎么做。
要實現(xiàn)多級分組,我們可以使用一個由雙參數(shù)版本的 Collectors.groupingBy 工廠方法創(chuàng)建的收集器,它除了普通的分類函數(shù)之外,還可以接受 collector 類型的第二個參數(shù)。那么要進行二級分組的話,我們可以把一個內(nèi)層 groupingBy 傳遞給外層 groupingBy ,并定義一個為流中項目分類的二級標準。
public static Map<Dish.Type, Map<CaloricLevel, List<Dish>>> duobleGroup(){Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect = menu.stream().collect(groupingBy(Dish::getType, groupingBy(dish -> {if (dish.getCalories() <= 400) {return CaloricLevel.DIET;} else if (dish.getCalories() <= 700) {return CaloricLevel.NORMAL;} else {return CaloricLevel.FAT;}})));System.out.println(collect);return collect;}我們把Dish的toString方法改寫一下
@Overridepublic String toString() {return name;}輸出
{MEAT={FAT=[pork], DIET=[chicken], NORMAL=[beef]}, OTHER={DIET=[rice, season fruit], NORMAL=[french fries, pizza]}, FISH={DIET=[prawns], NORMAL=[salmon]}}輸出的結(jié)果里的外層 Map 的鍵就是第一級分類函數(shù)生成的值:“fish, meat, other”, 而這個 Map 的值又是一個 Map ,鍵是二級分類函數(shù)生成的值:“normal, diet, fat”。最后,第二級 map 的值是流中元素構(gòu)成的 List ,是分別應用第一級和第二級分類函數(shù)所得到的對應第一級和第二級鍵的值:“salmon、pizza…” 這種多級分組操作可以擴展至任意層級,n級分組就會得到一個代表n級樹形結(jié)構(gòu)的n級Map 。
一般來說,把 groupingBy 看作“桶”比較容易明白。第一個 groupingBy 給每個鍵建立了一個桶。然后再用下游的收集器去收集每個桶中的元素,以此得到n級分組。
按子組收集數(shù)據(jù)
上個例子中,我們看到可以把第二個 groupingBy 收集器傳遞給外層收集器來實現(xiàn)多級分組。但進一步說,傳遞給第一個 groupingBy 的第二個收集器可以是任何類型,而不一定是另一個 groupingBy 。
例如,要數(shù)一數(shù)菜單中每類菜有多少個,可以傳遞 counting 收集器作為groupingBy 收集器的第二個參數(shù)
menu.stream().collect(groupingBy(Dish::getType, counting()))輸出
{FISH=2, MEAT=3, OTHER=4}還要注意,普通的單參數(shù) groupingBy(f) (其中 f 是分類函數(shù))實際上是 groupingBy(f,toList()) 的簡便寫法。
再舉一個例子,你可以把前面用于查找菜單中熱量最高的菜的收集器改一改,按照菜的類型分類:
// 按類型分類System.out.println(menu.stream().collect(groupingBy(Dish::getType)));// 按類型分類,獲取最高熱量的菜System.out.println(menu.stream().collect(groupingBy(Dish::getType, maxBy(Comparator.comparing(Dish::getCalories)))));輸出
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]} {FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}這個 Map 中的值是 Optional ,因為這是 maxBy 工廠方法生成的收集器的類型,但實際上,如果菜單中沒有某一類型的 Dish ,這個類型就不會對應一個 Optional. empty() 值,而且根本不會出現(xiàn)在 Map 的?中。 groupingBy 收集器只有在應用分組條件后,第一次在流中找到某個鍵對應的元素時才會把鍵加入到分組 Map 中。這意味著 Optional 包裝器在這里不是很有用,因為它不會僅僅因為它是歸約收集器的返回類型而表達一個最終可能不存在卻意外存在的值。
【把收集器的結(jié)果轉(zhuǎn)換為另一類型】
因為分組操作的 Map 結(jié)果中的每個值上包裝的 Optional 沒什么用,所以你可能想要把它們?nèi)サ簟R龅竭@一點,或者更一般地來說,把收集器返回的結(jié)果轉(zhuǎn)換為另一種類型,你可以使用Collectors.collectingAndThen 工廠方法返回的收集器
查找每個子組中熱量最高的 Dish
// 按類型分類,獲取最高熱量的菜System.out.println(menu.stream().collect(groupingBy(Dish::getType,collectingAndThen(maxBy(Comparator.comparing(Dish::getCalories)),Optional::get))));輸出
{FISH=salmon, OTHER=pizza, MEAT=pork}這個工廠方法接受兩個參數(shù)——要轉(zhuǎn)換的收集器以及轉(zhuǎn)換函數(shù),并返回另一個收集器。這個收集器相當于舊收集器的一個包裝, collect 操作的最后一步就是將返回值用轉(zhuǎn)換函數(shù)做一個映射。在這里,被包起來的收集器就是用 maxBy 建立的那個,而轉(zhuǎn)換函數(shù) Optional::get 則把返回的 Optional 中的值提取出來。
這個操作放在這里是安全的,因為 reducing收集器永遠都不會返回 Optional.empty() 。
圖解工作過程
從最外層開始逐層向里,注意以下幾點
- 收集器用虛線表示,因此 groupingBy 是最外層,根據(jù)菜肴的類型把菜單流分組,得到三個子流
- groupingBy 收集器包裹著 collectingAndThen 收集器,因此分組操作得到的每個子流都用這第二個收集器做進一步歸約
- collectingAndThen 收集器又包裹著第三個收集器 maxBy
- 隨后由歸約收集器進行子流的歸約操作,然后包含它的 collectingAndThen 收集器會對其結(jié)果應用 Optional:get 轉(zhuǎn)換函數(shù)。
- 對三個子流分別執(zhí)行這一過程并轉(zhuǎn)換而得到的三個值,也就是各個類型中熱量最高的Dish ,將成為 groupingBy 收集器返回的 Map 中與各個分類鍵( Dish 的類型)相關聯(lián)的值。
與 groupingBy聯(lián)合使用的其他收集器的例子
一般來說,通過 groupingBy 工廠方法的第二個參數(shù)傳遞的收集器將會對分到同一組中的所有流元素執(zhí)行進一步歸約操作。
例如,你還重用求出所有菜肴熱量總和的收集器,不過這次是對每一組 Dish 求和
menu.stream().collect(groupingBy(Dish::getType,summingInt(Dish::getCalories)));返回
{MEAT=1900, FISH=750, OTHER=1550}然而常常和 groupingBy 聯(lián)合使用的另一個收集器是 mapping 方法生成的。這個方法接受兩個參數(shù):
- 一個函數(shù)對流中的元素做變換
- 另一個則將變換的結(jié)果對象收集起來
其目的是在累加之前對每個輸入元素應用一個映射函數(shù),這樣就可以讓接受特定類型元素的收集器適應不同類型的對象。
比方說你想要知道,對于每種類型的 Dish 菜單中都有哪些 CaloricLevel 。我們可以把 groupingBy 和 mapping 收集器結(jié)合起來,如下所示:
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =menu.stream().collect(groupingBy(Dish::getType, mapping(dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT; },toSet() )));System.out.println(caloricLevelsByType);輸出:
{MEAT=[NORMAL, FAT, DIET], OTHER=[NORMAL, DIET], FISH=[NORMAL, DIET]}傳遞給映?方法的轉(zhuǎn)換函數(shù)將 Dish 映射成了它的CaloricLevel :生成的 CaloricLevel 流傳遞給一個 toSet 收集器,它和 toList 類似,不過是把流中的元素映射到一個 Set 而不是 List 中,以便僅保留各不相同的值。
請注意在上一個示例中,對于返回的 Set 是什么類型并沒有任何保證。但通過使用 toCollection ,你就可以有更多的控制。例如,你可以給它傳遞一個構(gòu)造函數(shù)引用來要求 HashSet :
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =menu.stream().collect(groupingBy(Dish::getType, mapping(dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT; },toCollection(HashSet::new))));System.out.println(caloricLevelsByType);輸出
{FISH=[DIET, NORMAL], OTHER=[DIET, NORMAL], MEAT=[DIET, FAT, NORMAL]}附
public static List<Dish> menu = Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),new Dish("beef", false, 700, Dish.Type.MEAT),new Dish("chicken", false, 400, Dish.Type.MEAT),new Dish("french fries", true, 530, Dish.Type.OTHER),new Dish("rice", true, 350, Dish.Type.OTHER),new Dish("season fruit", true, 120, Dish.Type.OTHER),new Dish("pizza", true, 550, Dish.Type.OTHER),new Dish("prawns", false, 300, Dish.Type.FISH),new Dish("salmon", false, 450, Dish.Type.FISH));總結(jié)
以上是生活随笔為你收集整理的Java 8 - 收集器Collectors_分组groupingBy的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 8 - 收集器Collecto
- 下一篇: Java 8 - 收集器Collecto