effective java 3-第7章 lambda和stream[47]Stream優(yōu)先于用Collection作為返回類型
????許多方法都返回元素的序列。在Java 8之前,這類方法明顯的返回類型是集合接口Collection、Set和List、Iterable以及數(shù)組類型。一般來說,很容易確定要返回這其中哪一種類型。標(biāo)椎是一個(gè)集合接口。如果某個(gè)方法只為for-each 循環(huán)或者返回序列而存在,無法用它來實(shí)現(xiàn)一些Collection方法(一般是contains(Object)) 那么就用Iterable 接口吧。如果返回的元素是基本類型值,或者有嚴(yán)格的性能要求,就使用數(shù)組。在Java8 中增加了Stream ,本質(zhì)上導(dǎo)致給序列化返回的方法選擇適當(dāng)返回類型的任務(wù)變得更復(fù)雜了。
????或許你曾聽說過,現(xiàn)在Stream 是返回元素序列最明顯的選擇了,但如第45條所述,Stream 并沒有淘汰迭代:要編寫優(yōu)秀的代碼必須巧妙地將Stream與迭代結(jié)合起來啟用。如果一個(gè)API 只返回一個(gè)Stream ,那些想要用for-each 循環(huán)遍歷返回序列的用戶肯定要失望了。因?yàn)镾tream 接口只在 Iterable 接口中包含了唯一一個(gè)抽象方法,Stream 對(duì)于該方法的規(guī)范也適用于Iterable 的。唯一一個(gè)可以讓程序員避免用for-each 循環(huán)遍歷Stream 的是Stream 無法擴(kuò)展 Iterable 接口。
????遺憾的是,這個(gè)問題還沒有適當(dāng)?shù)慕鉀Q辦法。乍看之下,好像給Stream 的Iterator方法傳入一個(gè)方法引用可以解決。這樣得到的代碼可能有點(diǎn)雜亂,不清晰,但也不算難以理解:
????
遺憾的是,如果想要編譯這段代碼,就會(huì)得到一條報(bào)錯(cuò)信息:
為了使代碼能夠進(jìn)行編譯,必須將方法引用轉(zhuǎn)換成適當(dāng)參數(shù)化的 Iterable
這個(gè)客戶端代碼可行,但是實(shí)際使用時(shí)過于雜亂、不清晰。更好的解決辦法是使用適配器方法。JDK 沒有提供這樣的方法,但是編寫起來很容易,使用在上述代碼中內(nèi)嵌的相同方法即可。注意,在適配器方法中沒有必要進(jìn)行轉(zhuǎn)換,因?yàn)镴ava的類型引用在這里正好派上了用場(chǎng):
????注意,第34條中 Anagrams 程序的Stream 版本是使用 Files.lines 方法讀取詞典,而迭代版本則使用了掃描器(scanner)。Files.lines 方法優(yōu)于掃描器,因?yàn)楹笳呙淄痰袅嗽谧x取文件過程中遇到的所有異常。最理想的方式是在迭代版本中也使用Files.lines。這是程序員在特定情況下所做的一種妥協(xié),比如當(dāng)API 只有Stream 能訪問序列,而他們想通過for-each 語(yǔ)句遍歷該序列的時(shí)候。
????反過來說,想要利用Stream pipeline 處理序列的程序員,也會(huì)被只提供Iterable的API 搞得束手無策。同樣地JDK 沒有提供適配器,但是編寫起來也很容易:
????如果在編寫一個(gè)返回對(duì)象序列的方法時(shí),就知道它只在Stream pipeline 中使用,當(dāng)然就可以放心地返回Stream 了。同樣地,當(dāng)返回序列的方法只在迭代中使用時(shí),則應(yīng)該返回Iterable。但如果是用公共的API 返回序列,則應(yīng)該為那些想要編寫Stream pipeline,以及想要編寫for-each 語(yǔ)句的用戶分別提供,除非有足夠的理由相信大多數(shù)用戶都想要使用相同的機(jī)制。
????Collection接口是 Iterable 的一個(gè)子類型,它有一個(gè)stream 方法,因此提供和了迭代和stream 訪問。對(duì)于公共的、返回序列的方法,Collection或者適當(dāng)?shù)淖宇愋屯ǔJ亲罴训姆祷仡愋汀?/strong>數(shù)組也通過Arrays.asList 和Stream.of 方法提供了簡(jiǎn)單的迭代和stream 訪問。如果返回的序列足夠小,容易存儲(chǔ),或許最好返回標(biāo)準(zhǔn)的集合實(shí)現(xiàn),如 ArrayList 和HashSet。但是千萬(wàn)別在內(nèi)存中保存巨大的序列,將它作為集合返回即可。
????如果返回的序列很大,但是能被準(zhǔn)確表述,可以考慮實(shí)現(xiàn)一個(gè)專用的集合。假設(shè)想要返回一個(gè)指定集合的冪集(power set),其中包含它所有的子集。{a,b,c} 的冪集是{ {},{a},,{c},{a,b} ,{a,c}, {b,c},{b,c}, {a,b,c}? }。如果集合中有n個(gè)元素,它的冪集就有2n個(gè)。因此,不必考慮將冪集保存在標(biāo)準(zhǔn)的集合實(shí)現(xiàn)中。但是,有了AbstractList 的協(xié)助,為此實(shí)現(xiàn)定制集合就很容易了。
????技巧在于,用冪集中每個(gè)元素的索引作為位向量,在索引中排第n位,表示源集合中第n位元素存在或不存在。實(shí)質(zhì)上,在二進(jìn)制數(shù)0至2n-1 和有n位元素的集合的冪集之間,有一個(gè)自然映射。代碼如下:
????
????注意,如果輸入值集合中超過30個(gè)元素,PowerSet.of 會(huì)拋出異常。這正是用Collection而不是用Stream 或Iterable 作為返回類型的缺點(diǎn):Collection 有一個(gè)返回 int 類型的size 方法,它限制返回的序列長(zhǎng)度為Integer.MAX_VALUE 或者 2^31 -1 。如果集合更大,甚至無限大,Collection規(guī)范確實(shí)允許 size 方法返回2^31 -1 ,但這并非是最令人滿意的解決方案。
????為了在AbstractCollection 上編寫一個(gè) Collection 實(shí)現(xiàn),除了 Iterable 必須的那一個(gè)方法之外,只需要再實(shí)現(xiàn)兩個(gè)方法: contains和size 。這些方法經(jīng)常很容易編寫出高效的實(shí)現(xiàn)。如果不可行,或許是因?yàn)闆]有在迭代發(fā)生之前先確定序列的內(nèi)容,返回Stream 或者 Iterable,感覺哪一種更自然即可。如果能選擇,可以嘗試著分別用兩個(gè)方法返回。????
????有時(shí)候在選擇返回類型時(shí),只需要看是否易于實(shí)現(xiàn)即可。例如,要編寫一個(gè)方法,用它返回一個(gè)輸入列表的所有(相鄰的)子列表。它只用三行代碼來生成這些子列表,并將它們放在一個(gè)標(biāo)準(zhǔn)的集合中,但存放這個(gè)集合所需的內(nèi)存是源列表大小的平方。這雖然沒有冪集那么糟糕,但顯然也是無法接受的。像給冪集實(shí)現(xiàn)定制的集合那樣,確實(shí)很繁瑣,這個(gè)可能還更甚,因?yàn)镴DK沒有提供基本的Iterator實(shí)現(xiàn)來支持。
????但是,實(shí)現(xiàn)輸入列表的所有子列表的Stream 是很簡(jiǎn)單的,盡管它確實(shí)需要有點(diǎn)洞察力。我們把包含列表第一個(gè)元素的子列表稱作列表的前綴。例如 (a,b,c) 的前綴是(a )? (a ,b) 和 (a, b ,c) 。同樣的,把包含最后一個(gè)元素的子列表稱作后綴。因此,(a, b, c) 的后綴是 (a, b ,c)? (b, c,) 和 (c )。 考察洞察力的是,列表的子列表不過是前綴的后綴(或者說后綴的前綴)和空列表。這一發(fā)現(xiàn)直接帶來了一個(gè)清晰且簡(jiǎn)潔的實(shí)現(xiàn):
????
????注意,它用Stream.concat 方法將空列表添加到返回的Stream 。另外還用flatMap方法(詳見45條)生成了一個(gè)包含了所有前綴的所有后綴的Stream。最后,通過映射IntStream.range 和 IntStream.rangeClosed返回的連續(xù)int 值的 Stream,生成了前綴和后綴。通俗地將,這一術(shù)語(yǔ)的意思就是指數(shù)為整數(shù)的標(biāo)準(zhǔn)for 循環(huán)的Stream 版本。因此,這個(gè)子列表實(shí)現(xiàn)本質(zhì)上與明顯的嵌套式for 循環(huán)相類似:
????????
????這個(gè)for 循環(huán)也可以直接翻譯成一個(gè)Stream。這樣得到的結(jié)果比前一個(gè)實(shí)現(xiàn)更加簡(jiǎn)潔,但是可讀性稍微差了一點(diǎn)。它本質(zhì)上與45條中笛卡爾積的Stream 代碼相類似:
????
????像前面的for 循環(huán)一樣,這段代碼也沒有發(fā)出空列表。為了修正這個(gè)錯(cuò)誤,也應(yīng)該使用concat,如前一個(gè)版本中那樣,或者用ranClosed調(diào)用中的(int)Math.signum(start) 代替1。
????子列表的這些Stream 實(shí)現(xiàn)都很好,但這兩者都需要用戶在任何更適合迭代的地方,采用Stream-to-Iterable適配器,或者用Stream。Stream-to-Iterable適配器不僅打斷了客戶端代碼,在我的機(jī)器(本書作者 Joshua Bloch)上循環(huán)的速度還降低了2.3倍。專門構(gòu)建的Collection實(shí)現(xiàn)(此處沒有展示)相當(dāng)繁瑣,但是運(yùn)行速度在我的機(jī)器上比基于Stream 的實(shí)現(xiàn)快了約1.4倍。
????總而言之,在編寫返回一系列元素的方法時(shí),要記住有些用戶可能想要當(dāng)做Stream 處理,而其他用戶可能想要迭代。要盡量?jī)蛇吋骖?。如果可以返回集合,就返回集合。如果集合中已?jīng)有元素,或者序列中的元素?cái)?shù)量很少,足以創(chuàng)建一個(gè)新的集合,那么就返回一個(gè)標(biāo)準(zhǔn)的集合,如ArrayList。否則就要考慮實(shí)現(xiàn)一個(gè)定制的集合,如冪集(power set)范例中所示。如果無法返回集合,就返回Stream或者 Iterable,感覺哪一種更自然即可。如果在未來的Java發(fā)行版本中,Stream 接口聲明被修改成擴(kuò)展了Iterable接口,就可以放心地返回Stream了,因?yàn)樗鼈冊(cè)试S進(jìn)行Stream 處理和迭代。