摘要:收集器用作高級(jí)歸約剛剛的結(jié)論又引出了優(yōu)秀的函數(shù)式設(shè)計(jì)的另一個(gè)好處更易復(fù)合和重用。更具體地說,對(duì)流調(diào)用方法將對(duì)流中的元素觸發(fā)一個(gè)歸約操作由來參數(shù)化。另一個(gè)常見的返回單個(gè)值的歸約操作是對(duì)流中對(duì)象的一個(gè)數(shù)值字段求和。
用流收集數(shù)據(jù)
我們?cè)谇耙徽轮袑W(xué)到,流可以用類似于數(shù)據(jù)庫的操作幫助你處理集合。你可以把Java 8的流看作花哨又懶惰的數(shù)據(jù)集迭代器。它們支持兩種類型的操作:中間操作(如 filter 或 map )和終端操作(如 count 、 findFirst 、 forEach 和 reduce )。中間操作可以鏈接起來,將一個(gè)流轉(zhuǎn)換為另一個(gè)流。這些操作不會(huì)消耗流,其目的是建立一個(gè)流水線。與此相反,終端操作會(huì)消耗流,以產(chǎn)生一個(gè)最終結(jié)果,例如返回流中的最大元素。它們通常可以通過優(yōu)化流水線來縮短計(jì)算時(shí)間。
我們已經(jīng)在前面用過了 collect 終端操作了,當(dāng)時(shí)主要是用來把 Stream 中所有的元素結(jié)合成一個(gè) List 。在本章中,你會(huì)發(fā)現(xiàn) collect 是一個(gè)歸約操作,就像 reduce 一樣可以接受各種做法作為參數(shù),將流中的元素累積成一個(gè)匯總結(jié)果。具體的做法是通過定義新的Collector 接口來定義的,因此區(qū)分 Collection 、 Collector 和 collect 是很重要的。
現(xiàn)在,我們來看一個(gè)例子,看看我們用collect和收集器能做什么。
對(duì)一個(gè)交易列表按照貨幣分組,獲得該貨幣所有的交易總額和(返回一個(gè) Map
將交易列表分成兩組:貴的和不貴的(返回一個(gè) Map
創(chuàng)建多級(jí)分組,比如按城市對(duì)交易分組,然后進(jìn)一步按照貴或不貴分組(返回一個(gè)
Map
我們首先來看一個(gè)利用收集器的例子,想象一下,你有一個(gè)Transaction構(gòu)成的List,并且想按照名義貨幣進(jìn)行分組。在沒有Lambda的Java里,哪怕像這種簡(jiǎn)單的用例實(shí)現(xiàn)起來都很啰嗦,就像下面這樣:
// 建立累積交易分組的Map Map> transactionsByCurrencies = new HashMap<>(16); // 迭代 Transaction 的 List for (Transaction transaction : transactions) { // 提取 Transaction的貨幣 Currency currency = transaction.getCurrency(); List transactionsForCurrency = transactionsByCurrencies.get(currency); // 如果分組 Map 中沒有這種貨幣的條目,就創(chuàng)建一個(gè) if (transactionsForCurrency == null) { transactionsForCurrency = new ArrayList<>(); transactionsByCurrencies.put(currency, transactionsForCurrency); } // 將當(dāng)前遍歷的 Transaction加入同一貨幣的 Transaction 的 List transactionsForCurrency.add(transaction); } System.out.println(transactionsByCurrencies);
如果你是一位經(jīng)驗(yàn)豐富的Java程序員,寫這種東西可能挺順手的,不過你必須承認(rèn),做這么簡(jiǎn)單的一件事就得寫很多代碼。更糟糕的是,讀起來比寫起來更費(fèi)勁!代碼的目的并不容易看出來,盡管換作白話的話是很直截了當(dāng)?shù)模骸鞍蚜斜碇械慕灰装簇泿欧纸M。”你在本章中會(huì)學(xué)到,用Stream 中 collect 方法的一個(gè)更通用的 Collector 參數(shù),你就可以用一句話實(shí)現(xiàn)完全相同的結(jié)果,而用不著使用上一章那個(gè) toList 的特殊情況了:
Map> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
這一比差得還真多,對(duì)吧?
收集器簡(jiǎn)介前一個(gè)例子清楚地展示了函數(shù)式編程相對(duì)于指令式編程的一個(gè)主要優(yōu)勢(shì):你只需指出希望的結(jié)果——“做什么”,而不用操心執(zhí)行的步驟——“如何做”。在上一個(gè)例子里,傳遞給 collect方法的參數(shù)是 Collector 接口的一個(gè)實(shí)現(xiàn),也就是給 Stream 中元素做匯總的方法。上一章里的toList 只是說“按順序給每個(gè)元素生成一個(gè)列表”;在本例中, groupingBy 說的是“生成一個(gè)Map ,它的鍵是(貨幣)桶,值則是桶中那些元素的列表”。要是做多級(jí)分組,指令式和函數(shù)式之間的區(qū)別就會(huì)更加明顯:由于需要好多層嵌套循環(huán)和條件,指令式代碼很快就變得更難閱讀、更難維護(hù)、更難修改。
收集器用作高級(jí)歸約剛剛的結(jié)論又引出了優(yōu)秀的函數(shù)式API設(shè)計(jì)的另一個(gè)好處:更易復(fù)合和重用。收集器非常有用,因?yàn)橛盟梢院?jiǎn)潔而靈活地定義collect用來生成結(jié)果集合的標(biāo)準(zhǔn)。更具體地說,對(duì)流調(diào)用collect方法將對(duì)流中的元素觸發(fā)一個(gè)歸約操作(由Collector來參數(shù)化)。一般來說, Collector 會(huì)對(duì)元素應(yīng)用一個(gè)轉(zhuǎn)換函數(shù)(很多時(shí)候是不體現(xiàn)任何效果的恒等轉(zhuǎn)換,例如 toList ),并將結(jié)果累積在一個(gè)數(shù)據(jù)結(jié)構(gòu)中,從而產(chǎn)生這一過程的最終輸出。例如,在前面所示的交易分組的例子中,轉(zhuǎn)換函數(shù)提取了每筆交易的貨幣,隨后使用貨幣作為鍵,將交易本身累積在生成的 Map 中。
歸約和匯總為了說明從 Collectors 工廠類中能創(chuàng)建出多少種收集器實(shí)例,我們重用一下前一章的例子:包含一張佳肴列表的菜單!就像你剛剛看到的,在需要將流項(xiàng)目重組成集合時(shí),一般會(huì)使用收集器( Stream 方法 collect的參數(shù))。再寬泛一點(diǎn)來說,但凡要把流中所有的項(xiàng)目合并成一個(gè)結(jié)果時(shí)就可以用。這個(gè)結(jié)果可以是任何類型,可以復(fù)雜如代表一棵樹的多級(jí)映射,或是簡(jiǎn)單如一個(gè)整數(shù)——也許代表了菜單的熱量總和。
我們先來舉一個(gè)簡(jiǎn)單的例子,利用 counting 工廠方法返回的收集器,數(shù)一數(shù)菜單里有多少
種菜:
long howManyDishes = menu.stream().collect(Collectors.counting());
這還可以寫得更為直接:
long howManyDishes = menu.stream().count();
counting 收集器在和其他收集器聯(lián)合使用的時(shí)候特別有用,后面會(huì)談到這一點(diǎn)。
查找流中的最大值和最小值假設(shè)你想要找出菜單中熱量最高的菜。你可以使用兩個(gè)收集器, Collectors.maxBy和Collectors.minBy ,來計(jì)算流中的最大或最小值。這兩個(gè)收集器接收一個(gè) Comparator 參數(shù)來
比較流中的元素。你可以創(chuàng)建一個(gè) Comparator來根據(jù)所含熱量對(duì)菜肴進(jìn)行比較,并把它傳遞給
Collectors.maxBy :
Listmenu = Dish.MENU; Comparator dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories); Optional mostCalorieDish = menu.stream().max(dishCaloriesComparator); System.out.println(mostCalorieDish.get());
你可能在想 Optional
另一個(gè)常見的返回單個(gè)值的歸約操作是對(duì)流中對(duì)象的一個(gè)數(shù)值字段求和。或者你可能想要求平均數(shù)。這種操作被稱為匯總操作。讓我們來看看如何使用收集器來表達(dá)匯總操作。
匯總Collectors 類專門為匯總提供了一個(gè)工廠方法: Collectors.summingInt 。它可接受一個(gè)把對(duì)象映射為求和所需 int 的函數(shù),并返回一個(gè)收集器;該收集器在傳遞給普通的 collect 方法后即執(zhí)行我們需要的匯總操作。舉個(gè)例子來說,你可以這樣求出菜單列表的總熱量:
Listmenu = Dish.MENU; int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
除了Collectors.summingInt,還有Collectors.summingLong 和Collectors.summingDouble 方法的作用完全一樣,可以用于求和字段為 long 或 double 的情況。
但匯總不僅僅是求和;還有 Collectors.averagingInt ,連同對(duì)應(yīng)的 averagingLong 和
averagingDouble 可以計(jì)算數(shù)值的平均數(shù):
Listmenu = Dish.MENU; double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
到目前為止,你已經(jīng)看到了如何使用收集器來給流中的元素計(jì)數(shù),找到這些元素?cái)?shù)值屬性的最大值和最小值,以及計(jì)算其總和和平均值。不過很多時(shí)候,你可能想要得到兩個(gè)或更多這樣的結(jié)果,而且你希望只需一次操作就可以完成。在這種情況下,你可以使用 summarizingInt 工廠方法返回的收集器。例如,通過一次 summarizing 操作你可以就數(shù)出菜單中元素的個(gè)數(shù),并得到菜肴熱量總和、平均值、最大值和最小值:
Listmenu = 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());
同樣,相應(yīng)的 summarizingLong 和 summarizingDouble 工廠方法有相關(guān)的LongSummaryStatistics 和 DoubleSummaryStatistics 類型,適用于收集的屬性是原始類型 long 或double 的情況。
連接字符串joining 工廠方法返回的收集器會(huì)把對(duì)流中每一個(gè)對(duì)象應(yīng)用 toString 方法得到的所有字符
串連接成一個(gè)字符串。這意味著你把菜單中所有菜肴的名稱連接起來,如下所示:
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
請(qǐng)注意, joining 在內(nèi)部使用了 StringBuilder 來把生成的字符串逐個(gè)追加起來。結(jié)果:
porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
但該字符串的可讀性并不好。幸好, joining 工廠方法有一個(gè)重載版本可以接受元素之間的
分界符,這樣你就可以得到一個(gè)逗號(hào)分隔的菜肴名稱列表:
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
結(jié)果:
pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
到目前為止,我們已經(jīng)探討了各種將流歸約到一個(gè)值的收集器。在下一節(jié)中,我們會(huì)展示為什么所有這種形式的歸約過程,其實(shí)都是 Collectors.reducing 工廠方法提供的更廣義歸約收集器的特殊情況。
廣義的歸約匯總事實(shí)上,我們已經(jīng)討論的所有收集器,都是一個(gè)可以用 reducing 工廠方法定義的歸約過程的特殊情況而已。 Collectors.reducing 工廠方法是所有這些特殊情況的一般化。可以說,先前討論的案例僅僅是為了方便程序員而已。(但是,請(qǐng)記得方便程序員和可讀性是頭等大事!)例如,可以用 reducing 方法創(chuàng)建的收集器來計(jì)算你菜單的總熱量,如下所示:
Listmenu = Dish.MENU; int totalCalories = menu.stream().collect(reducing( 0, Dish::getCalories, (i, j) -> i + j)); System.out.println(totalCalories);
它需要三個(gè)參數(shù):
第一個(gè)參數(shù)是歸約操作的起始值,也是流中沒有元素時(shí)的返回值,所以很顯然對(duì)于數(shù)值和而言0是一個(gè)合適的值。
第二個(gè)參數(shù)是Lambda的語法糖,將菜肴轉(zhuǎn)換成一個(gè)表示其所含熱量的 int 。
第三個(gè)參數(shù)是一個(gè) BinaryOperator ,將兩個(gè)項(xiàng)目累積成一個(gè)同類型的值。這里它就是
對(duì)兩個(gè) int 求和。
同樣,你可以使用下面這樣單參數(shù)形式的 reducing 來找到熱量最高的菜,如下所示:
OptionalmostCalorieDish = menu.stream().collect(reducing( (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
你可以把單參數(shù) reducing 工廠方法創(chuàng)建的收集器看作三參數(shù)方法的特殊情況,它把流中的第一個(gè)項(xiàng)目作為起點(diǎn),把恒等函數(shù)(即一個(gè)函數(shù)僅僅是返回其輸入?yún)?shù))作為一個(gè)轉(zhuǎn)換函數(shù)。
收集框架的靈活性:以不同的方法執(zhí)行同樣的操作
你還可以進(jìn)一步簡(jiǎn)化前面使用 reducing 收集器的求和例子——引用 Integer 類的 sum 方法,而不用去寫一個(gè)表達(dá)同一操作的Lambda表達(dá)式。這會(huì)得到以下程序:
int totalCalories2 = menu.stream() .collect(reducing(0, // 初始值 Dish::getCalories, // 轉(zhuǎn)換函數(shù) Integer::sum)); // 積累函數(shù)
使用語法糖,能幫助我們簡(jiǎn)化一部分代碼。
還有另外一種方法不使用收集器也能執(zhí)行相同操作——將菜肴流映射為每一道菜的熱量,然后用前一個(gè)版本中使用的方法引用來歸約得到的流:
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
請(qǐng)注意,就像流的任何單參數(shù) reduce 操作一樣, reduce(Integer::sum) 返回的不是 int而是 Optional
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
根據(jù)情況選擇最佳解決方案
這再次說明了,函數(shù)式編程(特別是Java 8的 Collections 框架中加入的基于函數(shù)式風(fēng)格原理設(shè)計(jì)的新API)通常提供了多種方法來執(zhí)行同一個(gè)操作。這個(gè)例子還說明,收集器在某種程度上比Stream 接口上直接提供的方法用起來更復(fù)雜,但好處在于它們能提供更高水平的抽象和概括,也更容易重用和自定義。在《Java8實(shí)戰(zhàn)》中的的建議是,盡可能為手頭的問題探索不同的解決方案,但在通用的方案里面,始終選擇最專門化的一個(gè)。無論是從可讀性還是性能上看,這一般都是最好的決定。例如,要計(jì)菜單的總熱量,我們更傾向于最后一個(gè)解決方案(使用 IntStream ),因?yàn)樗詈?jiǎn)明,也很可能最易讀。同時(shí),它也是性能最好的一個(gè),因?yàn)?IntStream 可以讓我們避免自動(dòng)拆箱操作,也就是從Integer到int的隱式轉(zhuǎn)換,它在這里毫無用處。
分組一個(gè)常見的數(shù)據(jù)庫操作是根據(jù)一個(gè)或多個(gè)屬性對(duì)集合中的項(xiàng)目進(jìn)行分組。就像前面講到按貨幣對(duì)交易進(jìn)行分組的例子一樣,如果用指令式風(fēng)格來實(shí)現(xiàn)的話,這個(gè)操作可能會(huì)很麻煩、啰嗦而且容易出錯(cuò)。但是,如果用Java 8所推崇的函數(shù)式風(fēng)格來重寫的話,就很容易轉(zhuǎn)化為一個(gè)非常容易看懂的語句。我們來看看這個(gè)功能的第二個(gè)例子:假設(shè)你要把菜單中的菜按照類型進(jìn)行分類,有肉的放一組,有魚的放一組,其他的都放另一組。用 Collectors.groupingBy 工廠方法返回的收集器就可以輕松地完成這項(xiàng)任務(wù),如下所示:
Map> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
其結(jié)果是下面的 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 方法傳遞了一個(gè) Function (以方法引用的形式),它提取了流中每一道 Dish 的 Dish.Type 。我們把這個(gè) Function 叫作分類函數(shù),因?yàn)樗脕戆蚜髦械脑胤殖刹煌慕M。分組操作的結(jié)果是一個(gè) Map ,把分組函數(shù)返回的值作為映射的鍵,把流中所有具有這個(gè)分類值的項(xiàng)目的列表作為對(duì)應(yīng)的映射值。在菜單分類的例子中,鍵就是菜的類型,值就是包含所有對(duì)應(yīng)類型的菜肴的列表。
但是,分類函數(shù)不一定像方法引用那樣可用,因?yàn)槟阆胗靡苑诸惖臈l件可能比簡(jiǎn)單的屬性訪問器要復(fù)雜。例如,你可能想把熱量不到400卡路里的菜劃分為“低熱量”(diet),熱量400到700卡路里的菜劃為“普通”(normal),高于700卡路里的劃為“高熱量”(fat)。由于 Dish 類的作者沒有把這個(gè)操作寫成一個(gè)方法,你無法使用方法引用,但你可以把這個(gè)邏輯寫成Lambda表達(dá)式:
public enum CaloricLevel { /** * 卡路里等級(jí) */ DIET, NORMAL, FAT } Map多級(jí)分組> 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; } }));
要實(shí)現(xiàn)多級(jí)分組,我們可以使用一個(gè)由雙參數(shù)版本的 Collectors.groupingBy 工廠方法創(chuàng)建的收集器,它除了普通的分類函數(shù)之外,還可以接受 collector 類型的第二個(gè)參數(shù)。那么要進(jìn)行二級(jí)分組的話,我們可以把一個(gè)內(nèi)層 groupingBy 傳遞給外層groupingBy ,并定義一個(gè)為流中項(xiàng)目分類的二級(jí)標(biāo)準(zhǔn)。
Map>> 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; } }) ) );
這個(gè)二級(jí)分組的結(jié)果就是像下面這樣的兩級(jí) 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 的鍵就是第一級(jí)分類函數(shù)生成的值:“fish, meat, other”,而這個(gè) Map 的值又是一個(gè) Map ,鍵是二級(jí)分類函數(shù)生成的值:“normal, diet, fat”。最后,第二級(jí) map 的值是流中元素構(gòu)成的 List ,是分別應(yīng)用第一級(jí)和第二級(jí)分類函數(shù)所得到的對(duì)應(yīng)第一級(jí)和第二級(jí)鍵的值:“salmon、pizza…” 這種多級(jí)分組操作可以擴(kuò)展至任意層級(jí),n級(jí)分組就會(huì)得到一個(gè)代表n級(jí)樹形結(jié)構(gòu)的n級(jí)Map 。
一般來說,把 groupingBy 看作“桶”比較容易明白。第一個(gè) groupingBy 給每個(gè)鍵建立了一個(gè)桶。然后再用下游的收集器去收集每個(gè)桶中的元素,以此得到n級(jí)分組。
按子組收集數(shù)據(jù)在上一節(jié)中,我們看到可以把第二個(gè) groupingBy 收集器傳遞給外層收集器來實(shí)現(xiàn)多級(jí)分組。但進(jìn)一步說,傳遞給第一個(gè) groupingBy 的第二個(gè)收集器可以是任何類型,而不一定是另一個(gè) groupingBy 。例如,要數(shù)一數(shù)菜單中每類菜有多少個(gè),可以傳遞 counting 收集器作為groupingBy 收集器的第二個(gè)參數(shù):
MaptypesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
其結(jié)果是下面的 Map :
{OTHER=4, MEAT=3, FISH=2}
還要注意,普通的單參數(shù) groupingBy(f) (其中 f 是分類函數(shù))實(shí)際上是 groupingBy(f,toList()) 的簡(jiǎn)便寫法。
再舉一個(gè)例子,你可以把前面用于查找菜單中熱量最高的菜肴的收集器改一改,按照菜的類型分類:
Map> mostCaloricByType = menu.stream() .collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
這個(gè)分組的結(jié)果顯然是一個(gè) map ,以 Dish 的類型作為鍵,以包裝了該類型中熱量最高的 Dish的 Optional
{OTHER=Optional[Dish{name="pizza"}], MEAT=Optional[Dish{name="pork"}], FISH=Optional[Dish{name="salmon"}]}
把收集器的結(jié)果轉(zhuǎn)換為另一種類型
因?yàn)榉纸M操作的 Map 結(jié)果中的每個(gè)值上包裝的 Optional 沒什么用,所以你可能想要把它們?nèi)サ簟R龅竭@一點(diǎn),或者更一般地來說,把收集器返回的結(jié)果轉(zhuǎn)換為另一種類型,你可以使用Collectors.collectingAndThen 工廠方法返回的收集器,如下所示。
查找每個(gè)子組中熱量最高的 Dish:
Listmenu = Dish.MENU; Map mostCaloricByType = menu.stream() .collect(groupingBy(Dish::getType, // 分類函數(shù) collectingAndThen( maxBy(comparingInt(Dish::getCalories)), // 包裝后的收集器 Optional::get))); // 轉(zhuǎn)換函數(shù)
這個(gè)工廠方法接受兩個(gè)參數(shù)——要轉(zhuǎn)換的收集器以及轉(zhuǎn)換函數(shù),并返回另一個(gè)收集器。這個(gè)收集器相當(dāng)于舊收集器的一個(gè)包裝, collect 操作的最后一步就是將返回值用轉(zhuǎn)換函數(shù)做一個(gè)映射。在這里,被包起來的收集器就是用 maxBy 建立的那個(gè),而轉(zhuǎn)換函數(shù) Optional::get 則把返回的 Optional 中的值提取出來。前面已經(jīng)說過,這個(gè)操作放在這里是安全的,因?yàn)?reducing收集器永遠(yuǎn)都不會(huì)返回 Optional.empty() 。其結(jié)果是下面的 Map :
{OTHER=Dish{name="pizza"}, MEAT=Dish{name="pork"}, FISH=Dish{name="salmon"}}
把好幾個(gè)收集器嵌套起來很常見,它們之間到底發(fā)生了什么可能不那么明顯。從最外層開始逐層向里,注意以下幾點(diǎn):
收集器用虛線表示,因此 groupingBy 是最外層,根據(jù)菜肴的類型把菜單流分組,得到三個(gè)子流。
groupingBy 收集器包裹著 collectingAndThen 收集器,因此分組操作得到的每個(gè)子流都用這第二個(gè)收集器做進(jìn)一步歸約。
collectingAndThen 收集器又包裹著第三個(gè)收集器 maxBy 。
隨后由歸約收集器進(jìn)行子流的歸約操作,然后包含它的 collectingAndThen 收集器會(huì)對(duì)其結(jié)果應(yīng)用 Optional:get 轉(zhuǎn)換函數(shù)。
對(duì)三個(gè)子流分別執(zhí)行這一過程并轉(zhuǎn)換而得到的三個(gè)值,也就是各個(gè)類型中熱量最高的Dish ,將成為 groupingBy 收集器返回的 Map 中與各個(gè)分類鍵( Dish 的類型)相關(guān)聯(lián)的值。
與 groupingBy 聯(lián)合使用的其他收集器的例子
一般來說,通過 groupingBy 工廠方法的第二個(gè)參數(shù)傳遞的收集器將會(huì)對(duì)分到同一組中的所有流元素執(zhí)行進(jìn)一步歸約操作。例如,你還重用求出所有菜肴熱量總和的收集器,不過這次是對(duì)每一組 Dish 求和:
MaptotalCaloriesByType = menu.stream() .collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
然而常常和 groupingBy 聯(lián)合使用的另一個(gè)收集器是 mapping 方法生成的。這個(gè)方法接受兩個(gè)參數(shù):一個(gè)函數(shù)對(duì)流中的元素做變換,另一個(gè)則將變換的結(jié)果對(duì)象收集起來。其目的是在累加之前對(duì)每個(gè)輸入元素應(yīng)用一個(gè)映射函數(shù),這樣就可以讓接受特定類型元素的收集器適應(yīng)不同類型的對(duì)象。我們來看一個(gè)使用這個(gè)收集器的實(shí)際例子。比方說你想要知道,對(duì)于每種類型的 Dish ,菜單中都有哪些 CaloricLevel 。我們可以把 groupingBy 和 mapping 收集器結(jié)合起來,如下所示:
Map> 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())));
傳遞給映射方法的轉(zhuǎn)換函數(shù)將 Dish 映射成了它的CaloricLevel :生成的CaloricLevel 流傳遞給一個(gè) toSet 收集器,它和 toList 類似,不過是把流中的元素累積到一個(gè) Set 而不是 List 中,以便僅保留各不相同的值。如先前的示例所示,這個(gè)映射收集器將會(huì)收集分組函數(shù)生成的各個(gè)子流中的元素,讓你得到這樣的 Map 結(jié)果:
{OTHER=[DIET, NORMAL], MEAT=[DIET, FAT, NORMAL], FISH=[DIET, NORMAL]}
由此你就可以輕松地做出選擇了。如果你想吃魚并且在減肥,那很容易找到一道菜;同樣,如果你饑腸轆轆,想要很多熱量的話,菜單中肉類部分就可以滿足你的饕餮之欲了。請(qǐng)注意在上一個(gè)示例中,對(duì)于返回的 Set 是什么類型并沒有任何保證。但通過使用 toCollection ,你就可以有更多的控制。例如,你可以給它傳遞一個(gè)構(gòu)造函數(shù)引用來要求 HashSet :
Map> 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))));
使用流收集數(shù)據(jù)這一章,內(nèi)容是比較多的,使用分組等特性能幫助我們簡(jiǎn)化很大一部分的工作,從而提高我們的開發(fā)效率。
代碼Github:chap6
Gitee:chap6
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/77145.html
摘要:使用流收集數(shù)據(jù)分區(qū)分區(qū)是分組的特殊情況由一個(gè)謂詞返回一個(gè)布爾值的函數(shù)作為分類函數(shù),它稱分區(qū)函數(shù)。這種情況下,累加器對(duì)象將會(huì)直接用作歸約過程的最終結(jié)果。這也意味著,將累加器不加檢查地轉(zhuǎn)換為結(jié)果是安全的。 使用流收集數(shù)據(jù) 分區(qū) 分區(qū)是分組的特殊情況:由一個(gè)謂詞(返回一個(gè)布爾值的函數(shù))作為分類函數(shù),它稱分區(qū)函數(shù)。分區(qū)函數(shù)返回一個(gè)布爾值,這意味著得到的分組 Map 的鍵類型是 Boolean ...
摘要:分區(qū)函數(shù)返回一個(gè)布爾值,這意味著得到的分組的鍵類型是,于是它最多可以分為兩組是一組,是一組。當(dāng)遍歷到流中第個(gè)元素時(shí),這個(gè)函數(shù)執(zhí)行時(shí)會(huì)有兩個(gè)參數(shù)保存歸約結(jié)果的累加器已收集了流中的前個(gè)項(xiàng)目,還有第個(gè)元素本身。 一、收集器簡(jiǎn)介 把列表中的交易按貨幣分組: Map transactionsByCurrencies = transactions.stream().collect(groupi...
摘要:依舊使用剛剛對(duì)蘋果排序的代碼。現(xiàn)在,要做的是篩選出所有的綠蘋果,也許你會(huì)這一個(gè)這樣的方法在之前,基本上都是這樣寫的,看起來也沒什么毛病。但是,現(xiàn)在又要篩選一下重量超過克的蘋果。 《Java8實(shí)戰(zhàn)》-讀書筆記第一章(01) 最近一直想寫點(diǎn)什么東西,卻不知該怎么寫,所以就寫寫關(guān)于看《Java8實(shí)戰(zhàn)》的筆記吧。 第一章內(nèi)容較多,因此打算分幾篇文章來寫。 為什么要關(guān)心Java8 自1996年J...
摘要:第六章抽象本章會(huì)介紹如何將語句組織成函數(shù)。關(guān)鍵字參數(shù)和默認(rèn)值目前為止,我們使用的參數(shù)都是位置參數(shù),因?yàn)樗鼈兊奈恢煤苤匾聦?shí)上比它們的名字更重要。參數(shù)前的星號(hào)將所有值放置在同一個(gè)元祖中。函數(shù)內(nèi)的變量被稱為局部變量。 第六章:抽象 本章會(huì)介紹如何將語句組織成函數(shù)。還會(huì)詳細(xì)介紹參數(shù)(parameter)和作用域(scope)的概念,以及遞歸的概念及其在程序中的用途。 懶惰即美德 斐波那契數(shù)...
摘要:之前,使用匿名類給蘋果排序的代碼是的,這段代碼看上去并不是那么的清晰明了,使用表達(dá)式改進(jìn)后或者是不得不承認(rèn),代碼看起來跟清晰了。這是由泛型接口內(nèi)部實(shí)現(xiàn)方式造成的。 # Lambda表達(dá)式在《Java8實(shí)戰(zhàn)》中第三章主要講的是Lambda表達(dá)式,在上一章節(jié)的筆記中我們利用了行為參數(shù)化來因?qū)Σ粩嘧兓男枨螅詈笪覀円彩褂玫搅薒ambda,通過表達(dá)式為我們簡(jiǎn)化了很多代碼從而極大地提高了我們的...
閱讀 991·2021-11-23 09:51
閱讀 3480·2021-11-22 12:04
閱讀 2724·2021-11-11 16:55
閱讀 2949·2019-08-30 15:55
閱讀 3235·2019-08-29 14:22
閱讀 3359·2019-08-28 18:06
閱讀 1248·2019-08-26 18:36
閱讀 2135·2019-08-26 12:08