Java8 新特性Stream流——用流收集數據(分組:groupingBy)

說明:本案例來自 《Java8 實戰》書籍,有需要的朋友到本書的朋友到相關網站購買

電子書的話本人百度網盤提供PDF:(鏈接失效請留言)

鏈接: https://pan.baidu.com/s/1rOji5sj0cOADI2l5dMuHqA 提取碼: efxc

解決下載限速問題查看這篇文章:https://blog.csdn.net/love_moon821/article/details/88896665

分組:一個常見的數據庫操作是根據一個或多個屬性對集合中的項目進行分組。

Dish類的定義查看這篇文章末尾:https://blog.csdn.net/love_moon821/article/details/90400641

導入了Collectors類的所有靜態工廠方法:
import static java.util.stream.Collectors.*;
這樣你就可以寫counting()而用不着寫Collectors.counting()之類的了

實例:

把菜單中的菜按照類型進行分類,有肉的放一組,有魚的放一組,其他的都放另一組。

Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));

其結果是下面的Map:
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],MEAT=[pork, beef, chicken]}

這裏,你給groupingBy方法傳遞了一個Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我們把這個Function叫作分類函數,因爲它用來把流中的元素分成不同的組。如圖6-4所示,分組操作的結果是一個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.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} ));
其結果是下面的Map:

{DIET=[chicken, rice, season fruit, prawns], NORMAL=[beef, french fries, pizza, salmon], FAT=[pork]}

多級分組,我們可以使用一個由雙參數版本的Collectors.groupingBy工廠方法創建的收集器,它除了普通的分類函數之外,還可以接受collector類型的第二個參數。那麼要進行二級分組的話,我們可以把一個內層groupingBy傳遞給外層groupingBy,並定義一個爲流中項目分類的二級標準。

代碼如下:

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(
        Collectors.groupingBy(
                Dish::getType, Collectors.groupingBy(
                        dish ->{
                            if(dish.getCalories() <= 400)
                                return CaloricLevel.DIET;
                            else if(dish.getCalories() <= 700)
                                return CaloricLevel.NORMAL;
                            else
                                return CaloricLevel.FAT;
                        }
                )
        ));

這個二級分組的結果就是像下面這樣的兩級Map:
{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
FISH={DIET=[prawns], NORMAL=[salmon]},
OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}

這裏的外層Map的鍵就是第一級分類函數生成的值:“fish, meat, other”,而這個Map的值又是一個Map,鍵是二級分類函數生成的值:“normal, diet, fat”。最後,第二級map的值是流中元素構成的List,是分別應用第一級和第二級分類函數所得到的對應第一級和第二級鍵的值:“salmon、pizza…” 這種多級分組操作可以擴展至任意層級, n級分組就會得到一個代表n級樹形結構的n級
Map。圖6-5顯示了爲什麼結構相當於n維表格,並強調了分組操作的分類目的。一般來說,把groupingBy看作“桶”比較容易明白。第一個groupingBy給每個鍵建立了一個桶。然後再用下游的收集器去收集每個桶中的元素,以此得到n級分組。

按子組收集數據

我們看到可以把第二個groupingBy收集器傳遞給外層收集器來實現多級分組。但進一步說,傳遞給第一個groupingBy的第二個收集器可以是任何類型,而不一定是另一個groupingBy。例如,要數一數菜單中每類菜有多少個,可以傳遞counting收集器作爲groupingBy收集器的第二個參數:

Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));

其結果是下面的Map:
{MEAT=3, FISH=2, OTHER=4}

把前面用於查找菜單中熱量最高的菜餚的收集器改一改,按照菜的類型分類:

Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories))));

這個分組的結果顯然是一個map,以Dish的類型作爲鍵,以包裝了該類型中熱量最高的Dish的Optional<Dish>作爲值:
{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}

把收集器的結果轉換爲另一種類型
因爲分組操作的Map結果中的每個值上包裝的Optional沒什麼用,所以你可能想要把它們去掉。要做到這一點,或者更一般地來說,把收集器返回的結果轉換爲另一種類型,你可以使用Collectors.collectingAndThen工廠方法返回的收集器,如下所示。

查找每個子組中熱量最高的Dish

Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
其結果是下面的Map:
{FISH=salmon, OTHER=pizza, MEAT=pork}

把好幾個收集器嵌套起來很常見,它們之間到底發生了什麼可能不那麼明顯。圖6-6可以直觀地展示它們是怎麼工作的。從最外層開始逐層向裏,注意以下幾點。

  •  收集器用虛線表示,因此groupingBy是最外層,根據菜餚的類型把菜單流分組,得到三個子流。
  •  groupingBy收集器包裹着collectingAndThen收集器,因此分組操作得到的每個子流都用這第二個收集器做進一步歸約。
  •  collectingAndThen收集器又包裹着第三個收集器maxBy
  •  隨後由歸約收集器進行子流的歸約操作,然後包含它的collectingAndThen收集器會對其結果應用Optional:get轉換函數。
  •  對三個子流分別執行這一過程並轉換而得到的三個值,也就是各個類型中熱量最高的Dish,將成爲groupingBy收集器返回的Map中與各個分類鍵(Dish的類型)相關聯的值。

求出所有菜餚熱量總和的收集器

Map<Dish.Type, Integer> totalCaloriesByType =
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
其結果是下面的Map:

{OTHER=1550, FISH=750, MEAT=1900}

對於每種類型的Dish,菜單中都有哪些CaloricLevel。我們可以把groupingBymapping收集器結合起來

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() )));
Map結果:
{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}

注意在上一個示例中,對於返回的Set是什麼類型並沒有任何保證。但通過使用toCollection,你就可以有更多的控制。例如,你可以給它傳遞一個構造函數引用來要求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) )));
Map結果:
{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章