effective java 3 - 第7章 lambda和stream[46] 優(yōu)先選擇無副作用的函數(shù)
????如果剛接觸Stream,可能比較難以掌握其中的竅門。就算只是用Stream pipeline 來表達(dá)計(jì)算就困難重重。當(dāng)你好不容易成功了,運(yùn)行程序之后,卻可能感到這么做并沒有享受到多大益處。Stream 并不只是一個(gè)API ,它是一種基于函數(shù)編程的模型。為了獲得Stream 帶來的描述性和速度,有時(shí)還有并行性,必須采用范型以及API。
????Stream 范型最重要的部分是把計(jì)算構(gòu)造成一系列變型,每一級(jí)結(jié)果都盡可能靠近上一級(jí)結(jié)果的純函數(shù)(pure function)。純函數(shù)是指其結(jié)果只取決于輸入的函數(shù):它不依賴任何可變的狀態(tài),也不更新任何狀態(tài)。為了做到這一點(diǎn),傳入Stream 操作的任何函數(shù)對(duì)象,無論是中間操作還是終止操作,都應(yīng)該是無副作用的。
????有時(shí)會(huì)看到如下代碼片段,它構(gòu)建了一張表格,顯示這些單詞在一個(gè)文本文件中出現(xiàn)的頻率:
????以上代碼有什么問題嗎?它畢竟使用了Stream、Lambda和方法引用,并且得出了正確的答案。簡(jiǎn)而言之,這根本不是Stream 代碼;只不過是偽裝成Stream 代碼的迭代式代碼。它并沒有享受到Stream API 帶來的優(yōu)勢(shì),代碼反而更長(zhǎng)了點(diǎn),可讀性也差了點(diǎn),并且比相應(yīng)的迭代化代碼更難維護(hù)。因?yàn)檫@段代碼利用一個(gè)改變外部狀態(tài)(頻率表)的Lambda,完成了在終止操作的forEach中的所有操作forEach操作的任務(wù)不只展示由Stream 執(zhí)行的計(jì)算結(jié)果,這在代碼中并非好事,改變狀態(tài)的Lambda 也是如此。那么這段代碼應(yīng)該是什么樣的呢?
????這個(gè)代碼片段的作用與前一個(gè)例子一樣,只是正確使用了Stream API,變得更加簡(jiǎn)潔、清晰。那么為什么有人會(huì)以其他的方式編寫呢?這是為了使用他們已經(jīng)熟悉的工具。Java程序員都知道如何使用for-each循環(huán),終止操作的forEach也與之類似。但forEach操作是終止操作中最沒有威力的,也是對(duì)Stream 最不友好的。它是顯式迭代,因而不適合并行。forEach操作應(yīng)該只用于報(bào)告Stream 計(jì)算的結(jié)果,而不是執(zhí)行計(jì)算。有時(shí)候,也可以將forEach用于其他目的,比如將Stream 計(jì)算的結(jié)果添加到之前已經(jīng)存在的集合中去。
????改進(jìn)過的代碼使用了一個(gè)收集器(collector),為了使用Stream,這是必須了解的一個(gè)新概念。Collectors API 很嚇人:它有39種方法,其中有些方法還帶有5個(gè)類型參數(shù)!好消息是,你不必完成搞懂這個(gè)API 就能享受它帶來的好處。對(duì)于初學(xué)者,可以忽略Collector接口,并把收集器當(dāng)做封裝縮減策略的一個(gè)黑盒子對(duì)象。在這里,縮減的意思是將Stream的元素合并到單個(gè)對(duì)象中去。收集器產(chǎn)生的對(duì)象一般是一個(gè)集合(即名稱收集器)。
????將Stream 的元素集中到一個(gè)真正的Collection 里去的收集器比較簡(jiǎn)單。有三個(gè)這樣的收集器:toList() , toSet() 和 toCollection(collectionFactory)。它們分別返回一個(gè)列表,一個(gè)集合和程序員指定的集合類型。了解了這些,就可以編寫Stream pipeline,從頻率表中提取排名前十的單詞列表了:
????
????注意,這里沒有給toList方法配上它的Collectors 類。靜態(tài)導(dǎo)入 Collectors 的所有成員是慣例也是明智的,因?yàn)檫@樣可以提升Stream pipeline 的可讀性。
????這段代碼中唯一有技巧的部分是傳給sorted的比較器 comparing(freq::get).reversed()。comparing方法是一個(gè)比較器構(gòu)造方法(詳見第14條),它帶有一個(gè)鍵提取函數(shù)。函數(shù)讀取一個(gè)單詞,“提取”實(shí)際上是一個(gè)表查找:有限制的方法引用 freq::get在頻率表中查找單詞,并返回該單詞在文件中出現(xiàn)的次數(shù)。最后,在比較器上調(diào)用reversed,按頻率高低對(duì)單詞進(jìn)行排序。后面的事情就簡(jiǎn)單了,只要限制Stream 為10個(gè)單詞,并將它們集中到一個(gè)列表中即可。
????上一段代碼是利用Scanner 的 Stream 方法來獲得Stream。這個(gè)方法是在Java 9 中增加的。如果使用的是更早的版本,可以把實(shí)現(xiàn) Iterator 的掃描器,翻譯成使用了類似于第47條中適配器的Stream (streamOf(Iterable<E>))。
????Collectors中的另外36種方法又是什么樣的呢?它們大多數(shù)是為了便于將Stream 映射到集合中,這遠(yuǎn)比集中到真實(shí)的集合中要復(fù)雜得多。每個(gè)Stream 元素都有一個(gè)關(guān)聯(lián)的鍵和值,多個(gè)Stream 元素可以關(guān)聯(lián)同一個(gè)鍵。
????最簡(jiǎn)單的映射收集器是toMap(keyMapper,valueMapper),它帶有兩個(gè)函數(shù),其中一個(gè)是將Stream 元素映射到鍵,另一個(gè)是將它映射到值。我們采用第34條 fromString 實(shí)現(xiàn)中的收集器,將枚舉的字符串形式映射到枚舉本身:
????
????如果Stream 中的每個(gè)元素都映射到一個(gè)唯一的鍵,那么這個(gè)形式簡(jiǎn)單的toMap是很完美的。如果多個(gè)Stream 元素映射到同一個(gè)鍵,pipeline就會(huì)拋出一個(gè)IllegalStateException異常將它終止。
????toMap 更復(fù)雜的形式,以及groupingBy方法,提供了更多處理這類沖突的策略。其中一種方式是除了給toMap 方法提供了鍵和值映射器之外,還提供一個(gè)合并函數(shù)(merge function)。合并函數(shù)是一個(gè)BinaryOprator<V>,這里的 V 是映射的值類型。合并函數(shù)將與鍵關(guān)聯(lián)的任何其他值與現(xiàn)有值合并起來,因此,將入合并函數(shù)是乘法,得到的值就是與該值映射的鍵關(guān)聯(lián)的所有值的積。
????帶有三個(gè)參數(shù)的toMap 形式,對(duì)于完成從鍵到與鍵關(guān)聯(lián)的被選元素的映射也是非常有用的。假設(shè)有一個(gè)Stream,代表不同歌唱家的長(zhǎng)篇,我們想得到一個(gè)從歌唱家到最暢銷唱片之間的映射。下面這個(gè)收集器就可以完成這項(xiàng)任務(wù)。
????
????注意,這個(gè)比較器使用了靜態(tài)工廠方法maxBy,這是從BinaryOperator 靜態(tài)導(dǎo)入的。該方法將Comparator<T> 轉(zhuǎn)換成一個(gè)BinaryOperator<T> ,用于計(jì)算指定比較器產(chǎn)生的最大值。在這個(gè)例子中,比較器是由比較器構(gòu)造方法comparing 返回的,它有一個(gè)鍵提取函數(shù)Album::sales 。這看起來有點(diǎn)繞,但是代碼的可讀性良好。不嚴(yán)格地說,它的意思是“將唱片的stream 轉(zhuǎn)換成一個(gè)映射,將每個(gè)歌唱家映射到銷量最佳的唱片”。這就非常接近問題陳述了。
????帶有三個(gè)參數(shù)的toMap形式還有另一種用途,即生成一個(gè)收集器,當(dāng)有沖突時(shí)強(qiáng)制“保留最后更新(last-write-wins)”。對(duì)于許多Stream 而言,結(jié)果是不確定的,但如果與映射函數(shù)的劍關(guān)聯(lián)的所有值都相同,或者都是可接受的,那么下面這個(gè)收集器的行為就正是你所要的:
????toMap的第三個(gè)也是最后一個(gè)形式是,帶有第四個(gè)參數(shù),這是一個(gè)映射工廠,在使用時(shí)要指定特殊的映射實(shí)現(xiàn),如EnumMap 或者 TreeMap。
????toMap 的前三種版本還有還有另外的變換形式,命名為 toConcurrentMap,能有效地并行運(yùn)行,并生成ConcurrentHashMap 實(shí)例。
????除了toMap 方法,Collectors API 還提供了 groupingBy方法,它返回收集器以生成映射,根據(jù)分類函數(shù)將元素分門別類。分類函數(shù)帶有一個(gè)元素,并返回其所屬的類別。這個(gè)類別就是元素的映射鍵。groupingBy方法最簡(jiǎn)單的版本是只有一個(gè)分類器,并返回一個(gè)映射,映射值為每個(gè)類別中所有元素的列表。下列代碼就是在地45條的Anagram 程序中用于生成映射(從按字母排序的單詞,映射到字母排序相同的單詞列表)的收集器:
????words.collect(groupingBy(wrod -> alphabetize(word)));
????如果要讓groupingBy 返回一個(gè)收集器,用它生成一個(gè)值而不是列表的映射,除了分類器之外,還可以指定一個(gè)下游收集器(downstream collector)。下游收集器從包含某個(gè)類別中所有元素的Stream中生成一個(gè)值。這個(gè)參數(shù)最簡(jiǎn)單的用法是傳入 toSet(),其結(jié)果生成一個(gè)映射,這個(gè)映射值為元素集合而非列表。
????另一種方法是傳入toCollection(collectionFactory) ,允許創(chuàng)建存放各元素類別的集合。這樣就可以自由選擇自己想要的任何集合類型了。帶兩個(gè)參數(shù)的groupingBy 版本的另一種簡(jiǎn)單用法是,傳入 counting() 作為下游收集器。這樣會(huì)生成一個(gè)映射,它將每個(gè)類別與該類別中的元素?cái)?shù)量關(guān)聯(lián)起來,而不是包含元素的集合。這正是在本條目開頭處頻率表范例中見到的:
????
????groupingBy的第三個(gè)版本,除了下游收集器之外,還可以指定一個(gè)映射工廠。注意,這個(gè)方法違背了標(biāo)準(zhǔn)的可伸縮列表模式:參數(shù)mapFactory 要在donwStream 參數(shù)之前,而不是在它之后。groupingBy 的這個(gè)版本可以控制所包圍的映射,以及所包圍的集合,因此,比如可以定義一個(gè)收集器,讓它返回值為 TreeSet的 TreeMap。
????groupingByConcurrent 方法提供了groupingBy 所有三種重載的變體。這些變體可以有效地并發(fā)運(yùn)行,生成ConcurrentHashMap實(shí)例。還有一種比較少用到的 groupingBy變體叫作 partitioningBy。除了分類方法之外,它還帶一個(gè)斷言(predicate),并返回一個(gè)鍵位Boolean 的映射。這個(gè)方法有兩個(gè)重載,其中一個(gè)除了帶有斷言外,還帶有下游收集器。
????counting方法返回的收集器僅作用于下游收集器。通過在Stream 上的count方法,直接就有相同的功能,因此壓根沒有理由使用 collect(counting())。這個(gè)屬性還有15種Collectors方法。其中包含9種方法其名稱以summing、averaging和summarizing開頭開頭(相應(yīng)的stream基本類型上就有相同的功能)。它們還包括reducing、filtering、mapping、flatMapping、和collectingAndThen方法。大多數(shù)程序員都能安全地避開這里的大多數(shù)方法。從設(shè)計(jì)的角度來看,這些收集器試圖部分復(fù)制收集器中Stream的功能,以便下游收集器可以成為“ministream”。
????目前已經(jīng)提到了3個(gè)Collectors方法。雖然它們都在Collectors中,但是并不包含集合。前兩個(gè)是minBy和maxBy,它們有一個(gè)比較器,并返回由比較器確定的Stream中的最少元素或者最多元素。它們是Stream 接口中min和max方法的粗略概述,也是BinaryOperator 中同名方法返回的二進(jìn)制操作符,與收集器相類似?;仡櫼幌略谧顣充N唱片范例中用過的BinaryOperator.maxBy方法。
????最后一個(gè)Collectors方法是joining,它只在CharSequence 實(shí)例的Stream 中操作,例如字符串。它以參數(shù)的形式返回一個(gè)簡(jiǎn)單地合并元素的收集器。其中一種參數(shù)形式帶有一個(gè)名為delimiter(分界符)的CharSequence參數(shù),它返回一個(gè)連接Stream元素并在相鄰元素之間插入分隔符的收集器。如果傳入一個(gè)逗號(hào)作為分隔符,收集器就會(huì)返回一個(gè)用逗號(hào)隔開的值字符串(但要注意,如果Stream中的任何元素種包含逗號(hào),這個(gè)字符串就會(huì)引起歧義)。 這三種參數(shù)形式,除了分隔符之外,還有一個(gè)前綴和一個(gè)后綴。最終的收集器生成的字符串,會(huì)像在打印集合時(shí)所得到的那樣,如[came, saw, conquered ]。
????總而言之,編寫Stream pipeline的本質(zhì)是無副作用的函數(shù)對(duì)象。這適用于傳入Stream及相關(guān)對(duì)象的所有函數(shù)對(duì)象。終止操作中的forEach應(yīng)該只用來報(bào)告由Stream 執(zhí)行的計(jì)算結(jié)果,而不是讓它執(zhí)行計(jì)算。為了正確地使用Stream ,必須了解收集器。最重要的收集器工廠是toList、toSet、toMap、groupingBy和joining。