第11篇 java 8----用流收集資料 -----分組
阿新 • • 發佈:2018-12-16
** * 一個常見的資料庫操作是根據一個或多個屬性對集合中的專案進行分組。就像前面講到按貨 * 幣對交易進行分組的例子一樣,如果用指令式風格來實現的話,這個操作可能會很麻煩、囉嗦而 * 且容易出錯。但是,如果用Java 8所推崇的函式式風格來重寫的話,就很容易轉化為一個非常容 * 易看懂的語句。我們來看看這個功能的第二個例子:假設你要把選單中的菜按照型別進行分類, * 有肉的放一組,有魚的放一組,其他的都放另一組。用Collectors.groupingBy工廠方法返回 * 的收集器就可以輕鬆地完成這項任務, */ public class Demo01 { public static void main(String[] args) { List<Dish> menues = Arrays.asList(new Dish("rice", true, 300, Dish.Type.FISH), new Dish("meate", false, 500, Dish.Type.MEAT), new Dish("dog", true, 30000, Dish.Type.FISH)); // test01(menues); // test02(menues); // test03(menues); // test04(menues); // test05(menues); // test06(menues); // test07(menues); test08(menues); } /** * 單級分組:按照菜的型別進行分組 * * @param menues */ public static void test01(List<Dish> menues) { Map<Dish.Type, List<Dish>> dishMap = menues.stream().collect(groupingBy(Dish::getType)); System.out.println(dishMap.toString()); } /** * 單級分組: 但是,分類函式不一定像方法引用那樣可用,因為你想用以分類的條件可能比簡單的屬性訪 * 問器要複雜。例如,你可能想把熱量不到400卡路里的菜劃分為“低熱量”(diet),熱量400到700 * 卡路里的菜劃為“普通”(normal),高於700卡路里的劃為“高熱量”(fat)。由於Dish類的作者 * 沒有把這個操作寫成一個方法,你無法使用方法引用,但你可以把這個邏輯寫成Lambda表示式: * * @param menues */ public static void test02(List<Dish> menues) { Map<CaloricLevel, List<Dish>> caloricLevelMap = menues.stream().collect(groupingBy(dish -> { if (dish.getCalories() < 400) { return CaloricLevel.DIET; } else if (dish.getCalories() < 700) { return CaloricLevel.NORMAL; } else { return CaloricLevel.FAT; } })); System.out.println(caloricLevelMap.toString()); } /** * 多級分組:你已經看到了如何對選單中的菜餚按照型別和熱量進行分組,但要是想同時按照這兩 * 個標準分類怎麼辦呢?分組的強大之處就在於它可以有效地組合: * 要實現多級分組,我們可以使用一個由雙引數版本的Collectors.groupingBy工廠方法創 * 建的收集器,它除了普通的分類函式之外,還可以接受collector型別的第二個引數。那麼要進 * 行二級分組的話,我們可以把一個內層groupingBy傳遞給外層groupingBy,並定義一個為流 * 中專案分類的二級標準,如程式碼 * * @param menues */ public static void test03(List<Dish> menues) { Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishTypeCaloricLevelMap = menues.stream().collect(Collectors.groupingBy(Dish::getType, groupingBy(dish -> { if (dish.getCalories() < 400) { System.out.println("DIET"); return CaloricLevel.DIET; } else if (dish.getCalories() < 700) { System.out.println("NORMAL"); return CaloricLevel.NORMAL; } else { System.out.println("FAT"); return CaloricLevel.FAT; } }))); System.out.println(dishTypeCaloricLevelMap); } /** * 多級分組:按子組收集資料中,我們看到可以把第二個groupingBy收集器傳遞給外層收集器來實現多級分 * 組。但進一步說,傳遞給第一個groupingBy的第二個收集器可以是任何型別,而不一定是另一 * 個groupingBy。例如,要數一數選單中每類菜有多少個,可以傳遞counting收集器作為 * groupingBy收集器的第二個引數:例如,要數一數選單中每類菜有多少個,可以傳遞counting收集器作為 * groupingBy收集器的第二個引數; * 還要注意,普通的單引數groupingBy(f)(其中f是分類函式)實際上是groupingBy(f, * toList())的簡便寫法。 * * @param menues */ public static void test04(List<Dish> menues) { Map<Dish.Type, Long> typeCountMap = menues.stream().collect(Collectors.groupingBy(Dish::getType, counting())); System.out.println(typeCountMap); } /** * 再舉一個例子,你可以把前面用於查詢選單中熱量最高的菜餚的收集器改一改,按照菜的類 * 型分類: * * @param menues */ public static void test05(List<Dish> menues) { Map<Dish.Type, Optional<Dish>> typeOptionalMap = menues.stream().collect(Collectors.groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories)))); System.out.println(typeOptionalMap.toString()); } /** * 這個分組的結果顯然是一個map,以Dish的型別作為鍵,以包裝了該型別中熱量最高的Dish * 的Optional<Dish>作為值: * 注意 這個Map中的值是Optional,因為這是maxBy工廠方法生成的收集器的型別,但實際上, * 如果選單中沒有某一型別的Dish,這個型別就不會對應一個Optional. empty()值, * 而且根本不會出現在Map的鍵中。groupingBy收集器只有在應用分組條件後,第一次在 * 流中找到某個鍵對應的元素時才會把鍵加入分組Map中。這意味著Optional包裝器在這 * 裡不是很有用,因為它不會僅僅因為它是歸約收集器的返回型別而表達一個最終可能不 * 存在卻意外存在的值. * 把收集器的結果轉換為另一種型別: * 因為分組操作的Map結果中的每個值上包裝的Optional沒什麼用,所以你可能想要把它們 * 去掉。要做到這一點,或者更一般地來說,把收集器返回的結果轉換為另一種型別,你可以使用 * Collectors.collectingAndThen工廠方法返回的收集器 */ public static void test06(List<Dish> menues) { Map<Dish.Type, Dish> typeDishlMap = menues.stream().collect(groupingBy(Dish::getType, collectingAndThen(maxBy(Comparator.comparing(Dish::getCalories)) , Optional::get))); System.out.println(typeDishlMap.toString()); } /** * 與groupingBy聯合使用的其他收集器的例子: * 一般來說,通過groupingBy工廠方法的第二個引數傳遞的收集器將會對分到同一組中的所 * 有流元素執行進一步歸約操作。例如,你還重用求出所有菜餚熱量總和的收集器,不過這次是對 * 每一組Dish求和 * * @param menues */ public static void test07(List<Dish> menues) { Map<Dish.Type, Integer> dishMap = menues.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories))); System.out.println(dishMap); } /** * 然而常常和groupingBy聯合使用的另一個收集器是mapping方法生成的。這個方法接受兩 * 個引數:一個函式對流中的元素做變換,另一個則將變換的結果物件收集起來。其目的是在累加 * 之前對每個輸入元素應用一個對映函式,這樣就可以讓接受特定型別元素的收集器適應不同型別 * 的物件。我們來看一個使用這個收集器的實際例子。比方說你想要知道,對於每種型別的Dish, * 選單中都有哪些CaloricLevel。我們可以把groupingBy和mapping收集器結合起來 *這裡,就像我們前面見到過的,傳遞給對映方法的轉換函式將Dish對映成了它的 * CaloricLevel:生成的CaloricLevel流傳遞給一個toSet收集器,它和toList類似,不過是 * 把流中的元素累積到一個Set而不是List中,以便僅保留各不相同的值。如先前的示例所示,這 * 個對映收集器將會收集分組函式生成的各個子流中的元素,讓你得到這樣的Map結果: * @param menues */ public static void test08(List<Dish> menues) { Map<Dish.Type, Set<CaloricLevel>> collectSet = menues.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(collectSet); } }https://github.com/wangrui0