咱們在前一章中學到,流能夠用相似於數據庫的操做幫助你處理集合。你能夠把Java 8的流看做花哨又懶惰的數據集迭代器。它們支持兩種類型的操做:中間操做(如 filter 或 map )和終端操做(如 count 、 findFirst 、 forEach 和 reduce )。中間操做能夠連接起來,將一個流轉換爲另外一個流。這些操做不會消耗流,其目的是創建一個流水線。與此相反,終端操做會消耗流,以產生一個最終結果,例如返回流中的最大元素。它們一般能夠經過優化流水線來縮短計算時間。java
咱們已經在前面用過了 collect 終端操做了,當時主要是用來把 Stream 中全部的元素結合成一個 List 。在本章中,你會發現 collect 是一個歸約操做,就像 reduce 同樣能夠接受各類作法做爲參數,將流中的元素累積成一個彙總結果。具體的作法是經過定義新的Collector 接口來定義的,所以區分 Collection 、 Collector 和 collect 是很重要的。git
如今,咱們來看一個例子,看看咱們用collect和收集器能作什麼。程序員
Map<Boolean, List<Transaction>> )。github
咱們首先來看一個利用收集器的例子,想象一下,你有一個Transaction構成的List,而且想按照名義貨幣進行分組。在沒有Lambda的Java裏,哪怕像這種簡單的用例實現起來都很囉嗦,就像下面這樣:數據庫
// 創建累積交易分組的Map Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>(16); // 迭代 Transaction 的 List for (Transaction transaction : transactions) { // 提取 Transaction的貨幣 Currency currency = transaction.getCurrency(); List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency); // 若是分組 Map 中沒有這種貨幣的條目,就建立一個 if (transactionsForCurrency == null) { transactionsForCurrency = new ArrayList<>(); transactionsByCurrencies.put(currency, transactionsForCurrency); } // 將當前遍歷的 Transaction加入同一貨幣的 Transaction 的 List transactionsForCurrency.add(transaction); } System.out.println(transactionsByCurrencies);
若是你是一位經驗豐富的Java程序員,寫這種東西可能挺順手的,不過你必須認可,作這麼簡單的一件事就得寫不少代碼。更糟糕的是,讀起來比寫起來更費勁!代碼的目的並不容易看出來,儘管換做白話的話是很直截了當的:「把列表中的交易按貨幣分組。」你在本章中會學到,用Stream 中 collect 方法的一個更通用的 Collector 參數,你就能夠用一句話實現徹底相同的結果,而用不着使用上一章那個 toList 的特殊狀況了:編程
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
這一比差得還真多,對吧?安全
前一個例子清楚地展現了函數式編程相對於指令式編程的一個主要優點:你只需指出但願的結果——「作什麼」,而不用操心執行的步驟——「如何作」。在上一個例子裏,傳遞給 collect方法的參數是 Collector 接口的一個實現,也就是給 Stream 中元素作彙總的方法。上一章裏的toList 只是說「按順序給每一個元素生成一個列表」;在本例中, groupingBy 說的是「生成一個Map ,它的鍵是(貨幣)桶,值則是桶中那些元素的列表」。要是作多級分組,指令式和函數式之間的區別就會更加明顯:因爲須要好多層嵌套循環和條件,指令式代碼很快就變得更難閱讀、更難維護、更難修改。數據結構
剛剛的結論又引出了優秀的函數式API設計的另外一個好處:更易複合和重用。收集器很是有用,由於用它能夠簡潔而靈活地定義collect用來生成結果集合的標準。更具體地說,對流調用collect方法將對流中的元素觸發一個歸約操做(由Collector來參數化)。通常來講, Collector 會對元素應用一個轉換函數(不少時候是不體現任何效果的恆等轉換,例如 toList ),並將結果累積在一個數據結構中,從而產生這一過程的最終輸出。例如,在前面所示的交易分組的例子中,轉換函數提取了每筆交易的貨幣,隨後使用貨幣做爲鍵,將交易自己累積在生成的 Map 中。app
爲了說明從 Collectors 工廠類中能建立出多少種收集器實例,咱們重用一下前一章的例子:包含一張佳餚列表的菜單!就像你剛剛看到的,在須要將流項目重組成集合時,通常會使用收集器( Stream 方法 collect的參數)。再寬泛一點來講,但凡要把流中全部的項目合併成一個結果時就能夠用。這個結果能夠是任何類型,能夠複雜如表明一棵樹的多級映射,或是簡單如一個整數——也許表明了菜單的熱量總和。框架
咱們先來舉一個簡單的例子,利用 counting 工廠方法返回的收集器,數一數菜單裏有多少
種菜:
long howManyDishes = menu.stream().collect(Collectors.counting());
這還能夠寫得更爲直接:
long howManyDishes = menu.stream().count();
counting 收集器在和其餘收集器聯合使用的時候特別有用,後面會談到這一點。
假設你想要找出菜單中熱量最高的菜。你可使用兩個收集器, Collectors.maxBy和Collectors.minBy ,來計算流中的最大或最小值。這兩個收集器接收一個 Comparator 參數來
比較流中的元素。你能夠建立一個 Comparator來根據所含熱量對菜餚進行比較,並把它傳遞給
Collectors.maxBy :
List<Dish> menu = Dish.MENU; Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories); Optional<Dish> mostCalorieDish = menu.stream().max(dishCaloriesComparator); System.out.println(mostCalorieDish.get());
你可能在想 Optional<Dish> 是怎麼回事。要回答這個問題,咱們須要問「要是 menu 爲空怎麼辦」。那就沒有要返回的菜了!Java 8引入了 Optional ,它是一個容器,能夠包含也能夠不包含值。這裏它完美地表明瞭可能也可能不返回菜餚的狀況。
另外一個常見的返回單個值的歸約操做是對流中對象的一個數值字段求和。或者你可能想要求平均數。這種操做被稱爲彙總操做。讓咱們來看看如何使用收集器來表達彙總操做。
Collectors 類專門爲彙總提供了一個工廠方法: Collectors.summingInt 。它可接受一個把對象映射爲求和所需 int 的函數,並返回一個收集器;該收集器在傳遞給普通的 collect 方法後即執行咱們須要的彙總操做。舉個例子來講,你能夠這樣求出菜單列表的總熱量:
List<Dish> menu = Dish.MENU; int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
除了Collectors.summingInt,還有Collectors.summingLong 和Collectors.summingDouble 方法的做用徹底同樣,能夠用於求和字段爲 long 或 double 的狀況。
但彙總不只僅是求和;還有 Collectors.averagingInt ,連同對應的 averagingLong 和
averagingDouble 能夠計算數值的平均數:
List<Dish> menu = Dish.MENU; double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
到目前爲止,你已經看到了如何使用收集器來給流中的元素計數,找到這些元素數值屬性的最大值和最小值,以及計算其總和和平均值。不過不少時候,你可能想要獲得兩個或更多這樣的結果,並且你但願只需一次操做就能夠完成。在這種狀況下,你可使用 summarizingInt 工廠方法返回的收集器。例如,經過一次 summarizing 操做你能夠就數出菜單中元素的個數,並獲得菜餚熱量總和、平均值、最大值和最小值:
List<Dish> menu = Dish.MENU; IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories)); System.out.println(menuStatistics.getMax()); System.out.println(menuStatistics.getAverage()); System.out.println(menuStatistics.getMin()); System.out.println(menuStatistics.getCount()); System.out.println(menuStatistics.getSum());
一樣,相應的 summarizingLong 和 summarizingDouble 工廠方法有相關的LongSummaryStatistics 和 DoubleSummaryStatistics 類型,適用於收集的屬性是原始類型 long 或double 的狀況。
joining 工廠方法返回的收集器會把對流中每個對象應用 toString 方法獲得的全部字符
串鏈接成一個字符串。這意味着你把菜單中全部菜餚的名稱鏈接起來,以下所示:
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
請注意, joining 在內部使用了 StringBuilder 來把生成的字符串逐個追加起來。結果:
porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
但該字符串的可讀性並很差。幸虧, joining 工廠方法有一個重載版本能夠接受元素之間的
分界符,這樣你就能夠獲得一個逗號分隔的菜餚名稱列表:
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
結果:
pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
到目前爲止,咱們已經探討了各類將流歸約到一個值的收集器。在下一節中,咱們會展現爲何全部這種形式的歸約過程,其實都是 Collectors.reducing 工廠方法提供的更廣義歸約收集器的特殊狀況。
事實上,咱們已經討論的全部收集器,都是一個能夠用 reducing 工廠方法定義的歸約過程的特殊狀況而已。 Collectors.reducing 工廠方法是全部這些特殊狀況的通常化。能夠說,先前討論的案例僅僅是爲了方便程序員而已。(可是,請記得方便程序員和可讀性是頭等大事!)例如,能夠用 reducing 方法建立的收集器來計算你菜單的總熱量,以下所示:
List<Dish> menu = Dish.MENU; int totalCalories = menu.stream().collect(reducing( 0, Dish::getCalories, (i, j) -> i + j)); System.out.println(totalCalories);
它須要三個參數:
對兩個 int 求和。
一樣,你可使用下面這樣單參數形式的 reducing 來找到熱量最高的菜,以下所示:
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing( (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
你能夠把單參數 reducing 工廠方法建立的收集器看做三參數方法的特殊狀況,它把流中的第一個項目做爲起點,把恆等函數(即一個函數僅僅是返回其輸入參數)做爲一個轉換函數。
收集框架的靈活性:以不一樣的方法執行一樣的操做
你還能夠進一步簡化前面使用 reducing 收集器的求和例子——引用 Integer 類的 sum 方法,而不用去寫一個表達同一操做的Lambda表達式。這會獲得如下程序:
int totalCalories2 = menu.stream() .collect(reducing(0, // 初始值 Dish::getCalories, // 轉換函數 Integer::sum)); // 積累函數
使用語法糖,能幫助咱們簡化一部分代碼。
還有另一種方法不使用收集器也能執行相同操做——將菜餚流映射爲每一道菜的熱量,而後用前一個版本中使用的方法引用來歸約獲得的流:
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
請注意,就像流的任何單參數 reduce 操做同樣, reduce(Integer::sum) 返回的不是 int而是 Optional<Integer> ,以便在空流的狀況下安全地執行歸約操做。而後你只需用 Optional對象中的 get 方法來提取裏面的值就好了。請注意,在這種狀況下使用 get 方法是安全的,只是由於你已經肯定菜餚流不爲空。通常來講,使用容許提供默認值的方法,如 orElse 或 orElseGet來解開Optional中包含的值更爲安全。最後,更簡潔的方法是把流映射到一個 IntStream ,而後調用 sum 方法,你也能夠獲得相同的結果:
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
根據狀況選擇最佳解決方案
這再次說明了,函數式編程(特別是Java 8的 Collections 框架中加入的基於函數式風格原理設計的新API)一般提供了多種方法來執行同一個操做。這個例子還說明,收集器在某種程度上比Stream 接口上直接提供的方法用起來更復雜,但好處在於它們能提供更高水平的抽象和歸納,也更容易重用和自定義。在《Java8實戰》中的的建議是,儘量爲手頭的問題探索不一樣的解決方案,但在通用的方案裏面,始終選擇最專門化的一個。不管是從可讀性仍是性能上看,這通常都是最好的決定。例如,要計菜單的總熱量,咱們更傾向於最後一個解決方案(使用 IntStream ),由於它最簡明,也極可能最易讀。同時,它也是性能最好的一個,由於 IntStream 可讓咱們避免自動拆箱操做,也就是從Integer到int的隱式轉換,它在這裏毫無用處。
一個常見的數據庫操做是根據一個或多個屬性對集合中的項目進行分組。就像前面講到按貨幣對交易進行分組的例子同樣,若是用指令式風格來實現的話,這個操做可能會很麻煩、囉嗦並且容易出錯。可是,若是用Java 8所推崇的函數式風格來重寫的話,就很容易轉化爲一個很是容易看懂的語句。咱們來看看這個功能的第二個例子:假設你要把菜單中的菜按照類型進行分類,有肉的放一組,有魚的放一組,其餘的都放另外一組。用 Collectors.groupingBy 工廠方法返回的收集器就能夠輕鬆地完成這項任務,以下所示:
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
其結果是下面的 Map:
{OTHER=[Dish{name='french fries'}, Dish{name='rice'}, Dish{name='season fruit'}, Dish{name='pizza'}], MEAT=[Dish{name='pork'}, Dish{name='beef'}, Dish{name='chicken'}], FISH=[Dish{name='prawns'}, Dish{name='salmon'}]}
這裏,你給 groupingBy 方法傳遞了一個 Function (以方法引用的形式),它提取了流中每一道 Dish 的 Dish.Type 。咱們把這個 Function 叫做分類函數,由於它用來把流中的元素分紅不一樣的組。分組操做的結果是一個 Map ,把分組函數返回的值做爲映射的鍵,把流中全部具備這個分類值的項目的列表做爲對應的映射值。在菜單分類的例子中,鍵就是菜的類型,值就是包含全部對應類型的菜餚的列表。
可是,分類函數不必定像方法引用那樣可用,由於你想用以分類的條件可能比簡單的屬性訪問器要複雜。例如,你可能想把熱量不到400卡路里的菜劃分爲「低熱量」(diet),熱量400到700卡路里的菜劃爲「普通」(normal),高於700卡路里的劃爲「高熱量」(fat)。因爲 Dish 類的做者沒有把這個操做寫成一個方法,你沒法使用方法引用,但你能夠把這個邏輯寫成Lambda表達式:
public enum CaloricLevel { /** * 卡路里等級 */ DIET, NORMAL, FAT } Map<Dish.CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect( groupingBy(dish -> { if (dish.getCalories() <= 400) { return Dish.CaloricLevel.DIET; } else if (dish.getCalories() <= 700) { return Dish.CaloricLevel.NORMAL; } else { return Dish.CaloricLevel.FAT; } }));
要實現多級分組,咱們可使用一個由雙參數版本的 Collectors.groupingBy 工廠方法建立的收集器,它除了普通的分類函數以外,還能夠接受 collector 類型的第二個參數。那麼要進行二級分組的話,咱們能夠把一個內層 groupingBy 傳遞給外層groupingBy ,並定義一個爲流中項目分類的二級標準。
Map<Dish.Type, Map<Dish.CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect( groupingBy(Dish::getType, groupingBy(dish -> { if (dish.getCalories() <= 400) { return Dish.CaloricLevel.DIET; } else if (dish.getCalories() <= 700) { return Dish.CaloricLevel.NORMAL; } else { return Dish.CaloricLevel.FAT; } }) ) );
這個二級分組的結果就是像下面這樣的兩級 Map :
{OTHER={DIET=[Dish{name='rice'}, Dish{name='season fruit'}], NORMAL=[Dish{name='french fries'}, Dish{name='pizza'}]}, MEAT={DIET=[Dish{name='chicken'}], FAT=[Dish{name='pork'}], NORMAL=[Dish{name='beef'}]}, FISH={DIET=[Dish{name='prawns'}], NORMAL=[Dish{name='salmon'}]}}
這裏的外層 Map 的鍵就是第一級分類函數生成的值:「fish, meat, other」,而這個 Map 的值又是一個 Map ,鍵是二級分類函數生成的值:「normal, diet, fat」。最後,第二級 map 的值是流中元素構成的 List ,是分別應用第一級和第二級分類函數所獲得的對應第一級和第二級鍵的值:「salmon、pizza…」 這種多級分組操做能夠擴展至任意層級,n級分組就會獲得一個表明n級樹形結構的n級Map 。
通常來講,把 groupingBy 看做「桶」比較容易明白。第一個 groupingBy 給每一個鍵創建了一個桶。而後再用下游的收集器去收集每一個桶中的元素,以此獲得n級分組。
在上一節中,咱們看到能夠把第二個 groupingBy 收集器傳遞給外層收集器來實現多級分組。但進一步說,傳遞給第一個 groupingBy 的第二個收集器能夠是任何類型,而不必定是另外一個 groupingBy 。例如,要數一數菜單中每類菜有多少個,能夠傳遞 counting 收集器做爲groupingBy 收集器的第二個參數:
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
其結果是下面的 Map :
{OTHER=4, MEAT=3, FISH=2}
還要注意,普通的單參數 groupingBy(f) (其中 f 是分類函數)其實是 groupingBy(f,toList()) 的簡便寫法。
再舉一個例子,你能夠把前面用於查找菜單中熱量最高的菜餚的收集器改一改,按照菜的類型分類:
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream() .collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
這個分組的結果顯然是一個 map ,以 Dish 的類型做爲鍵,以包裝了該類型中熱量最高的 Dish的 Optional<Dish> 做爲值:
{OTHER=Optional[Dish{name='pizza'}], MEAT=Optional[Dish{name='pork'}], FISH=Optional[Dish{name='salmon'}]}
把收集器的結果轉換爲另外一種類型
由於分組操做的 Map 結果中的每一個值上包裝的 Optional 沒什麼用,因此你可能想要把它們去掉。要作到這一點,或者更通常地來講,把收集器返回的結果轉換爲另外一種類型,你可使用Collectors.collectingAndThen 工廠方法返回的收集器,以下所示。
查找每一個子組中熱量最高的 Dish:
List<Dish> menu = Dish.MENU; Map<Dish.Type, Dish> mostCaloricByType = menu.stream() .collect(groupingBy(Dish::getType, // 分類函數 collectingAndThen( maxBy(comparingInt(Dish::getCalories)), // 包裝後的收集器 Optional::get))); // 轉換函數
這個工廠方法接受兩個參數——要轉換的收集器以及轉換函數,並返回另外一個收集器。這個收集器至關於舊收集器的一個包裝, collect 操做的最後一步就是將返回值用轉換函數作一個映射。在這裏,被包起來的收集器就是用 maxBy 創建的那個,而轉換函數 Optional::get 則把返回的 Optional 中的值提取出來。前面已經說過,這個操做放在這裏是安全的,由於 reducing收集器永遠都不會返回 Optional.empty() 。其結果是下面的 Map :
{OTHER=Dish{name='pizza'}, MEAT=Dish{name='pork'}, FISH=Dish{name='salmon'}}
把好幾個收集器嵌套起來很常見,它們之間到底發生了什麼可能不那麼明顯。從最外層開始逐層向裏,注意如下幾點:
與 groupingBy 聯合使用的其餘收集器的例子
通常來講,經過 groupingBy 工廠方法的第二個參數傳遞的收集器將會對分到同一組中的全部流元素執行進一步歸約操做。例如,你還重用求出全部菜餚熱量總和的收集器,不過此次是對每一組 Dish 求和:
Map<Dish.Type, Integer> totalCaloriesByType = menu.stream() .collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
然而經常和 groupingBy 聯合使用的另外一個收集器是 mapping 方法生成的。這個方法接受兩個參數:一個函數對流中的元素作變換,另外一個則將變換的結果對象收集起來。其目的是在累加以前對每一個輸入元素應用一個映射函數,這樣就可讓接受特定類型元素的收集器適應不一樣類型的對象。咱們來看一個使用這個收集器的實際例子。比方說你想要知道,對於每種類型的 Dish ,菜單中都有哪些 CaloricLevel 。咱們能夠把 groupingBy 和 mapping 收集器結合起來,以下所示:
Map<Dish.Type, Set<Dish.CaloricLevel>> caloricLevelsByType = menu.stream().collect( groupingBy(Dish::getType, mapping( dish -> { if (dish.getCalories() <= 400) { return Dish.CaloricLevel.DIET; } else if (dish.getCalories() <= 700) { return Dish.CaloricLevel.NORMAL; } else { return Dish.CaloricLevel.FAT; } }, toSet())));
傳遞給映射方法的轉換函數將 Dish 映射成了它的CaloricLevel :生成的CaloricLevel 流傳遞給一個 toSet 收集器,它和 toList 相似,不過是把流中的元素累積到一個 Set 而不是 List 中,以便僅保留各不相同的值。如先前的示例所示,這個映射收集器將會收集分組函數生成的各個子流中的元素,讓你獲得這樣的 Map 結果:
{OTHER=[DIET, NORMAL], MEAT=[DIET, FAT, NORMAL], FISH=[DIET, NORMAL]}
由此你就能夠輕鬆地作出選擇了。若是你想吃魚而且在減肥,那很容易找到一道菜;一樣,若是你飢腸轆轆,想要不少熱量的話,菜單中肉類部分就能夠知足你的饕餮之慾了。請注意在上一個示例中,對於返回的 Set 是什麼類型並無任何保證。但經過使用 toCollection ,你就能夠有更多的控制。例如,你能夠給它傳遞一個構造函數引用來要求 HashSet :
Map<Dish.Type, Set<Dish.CaloricLevel>> caloricLevelsByType = menu.stream().collect( groupingBy(Dish::getType, mapping( dish -> { if (dish.getCalories() <= 400) { return Dish.CaloricLevel.DIET; } else if (dish.getCalories() <= 700) { return Dish.CaloricLevel.NORMAL; } else { return Dish.CaloricLevel.FAT; } }, toCollection(HashSet::new))));
使用流收集數據這一章,內容是比較多的,使用分組等特性能幫助咱們簡化很大一部分的工做,從而提升咱們的開發效率。
Github:chap6
Gitee:chap6