??許多方法返回元素序列井联。在Java 8之前瓮栗,這些方法的明顯返回類型是collection接口collection廉羔、Set和List;Iterable;以及數(shù)組類型。通常塘揣,很容易決定返回哪一種類型包雀。規(guī)范是一個(gè)集合接口。如果方法的存在只是為了啟用for-each循環(huán)亲铡,或者無法使返回的序列實(shí)現(xiàn)某些集合方法(通常是contains(Object))才写,則使用迭代接口。如果返回的元素是原始值或有嚴(yán)格的性能要求奖蔓,則使用數(shù)組赞草。在Java 8中,流被添加到平臺(tái)中吆鹤,這使得為序列返回方法選擇合適的返回類型的任務(wù)變得非常復(fù)雜厨疙。
??您可能聽說過,流現(xiàn)在是返回元素序列的最佳選擇疑务,但是正如第45項(xiàng)中所討論的沾凄,流不會(huì)使迭代過時(shí):編寫好的代碼需要明智地組合流和迭代。如果一個(gè)API只返回一個(gè)流知允,而一些用戶希望用for-each循環(huán)遍歷返回的序列撒蟀,那么這些用戶將會(huì)有理由感到不安。尤其令人沮喪的是廊镜,Stream接口在Iterable接口中包含唯一的抽象方法牙肝,而且Stream對(duì)該方法的規(guī)范與Iterable兼容。阻止程序員使用for-each循環(huán)在流上迭代的惟一原因是流沒有擴(kuò)展Iterable。
??遺憾的是配椭,對(duì)于這個(gè)問題沒有好的解決辦法虫溜。乍一看,將方法引用傳遞給Stream的迭代器方法似乎是可行的股缸。生成的代碼可能有點(diǎn)嘈雜和不透明衡楞,但并非不合理:
??不幸的是,如果你試圖編譯這段代碼敦姻,你會(huì)得到一個(gè)錯(cuò)誤信息:
??為了使代碼編譯瘾境,您必須將方法引用轉(zhuǎn)換為適當(dāng)?shù)膮?shù)化迭代:
??這個(gè)客戶機(jī)代碼可以工作,但是它太吵镰惦,而且不透明迷守,不能在實(shí)踐中使用。更好的解決方法是使用適配器方法旺入。JDK沒有提供這樣的方法兑凿,但是使用上面代碼片段中使用的內(nèi)聯(lián)技術(shù)很容易編寫這樣的方法。注意茵瘾,適配器方法中不需要強(qiáng)制轉(zhuǎn)換礼华,因?yàn)镴ava的類型推斷在這種上下文中正常工作:
??使用這個(gè)適配器,您可以使用for-each語句遍歷任何流:
??注意拗秘,第34項(xiàng)中的字謎程序的流版本使用了這些Files.lines 方法讀取字典圣絮,而迭代版使用scanner。這些 Files.lines 方法優(yōu)于scanner雕旨,scanner可以在讀取文件時(shí)無聲地處理遇到的任何異常扮匠。我們會(huì)使用 Files.lines 版本中的行也是如此。如果一個(gè)API只提供對(duì)序列的流訪問奸腺,并且希望用for-each語句遍歷序列餐禁,那么程序員就會(huì)做出這種妥協(xié)。
??相反突照,想要使用流管道處理序列的程序員會(huì)被只提供可迭代的API打亂帮非,這是可以理解的。同樣讹蘑,JDK沒有提供適配器末盔,但是很容易編寫適配器:
??如果您正在編寫一個(gè)返回對(duì)象序列的方法,并且您知道它只會(huì)在流管道中使用座慰,那么您當(dāng)然應(yīng)該可以隨意返回流陨舱。類似地,返回只用于迭代的序列的方法應(yīng)該返回一個(gè)Iterable.但是如果你寫一個(gè)公共API,它返回一個(gè)序列,你應(yīng)該提供用戶想寫流管道以及那些想寫for - each語句,除非你有充分的理由相信大多數(shù)用戶想要使用相同的機(jī)制版仔。
??Collection 接口是Iterable的子類型游盲,并且有一個(gè)流方法误墓,因此它同時(shí)提供了迭代和流訪問。所以, Collection 或適當(dāng)?shù)淖宇愋屯ǔJ枪驳姆祷匦蛄蟹椒ǖ淖罴逊祷仡愋汀?/strong>數(shù)組還提供了簡(jiǎn)單的迭代和流訪問數(shù)組還提供了簡(jiǎn)單的迭代和流訪問Arrays.asList和 Stream.of的方法益缎。如果返回的序列足夠小谜慌,可以輕松地放入內(nèi)存,那么最好返回標(biāo)準(zhǔn)集合實(shí)現(xiàn)之一莺奔,比如ArrayList或HashSet欣范。但是不要將一個(gè)大序列存儲(chǔ)在內(nèi)存中,只是將它作為一個(gè)集合返回令哟。
??如果返回的序列很大恼琼,但是可以用簡(jiǎn)潔的方式表示,那么可以考慮實(shí)現(xiàn)一個(gè)特殊用途的集合屏富。例如晴竞,假設(shè)您想返回一個(gè)給定集合的冪集,它由它的所有子集組成役听。冪集為{a, b, c} {{}, {}, 颓鲜, {c}, {a、b}, {a, c}, {b, c}, {a, b, c}}典予。如果一個(gè)集合有n個(gè)元素,它的冪集就是2^n乐严。因此瘤袖,您甚至不應(yīng)該考慮將power集存儲(chǔ)在標(biāo)準(zhǔn)集合實(shí)現(xiàn)中。但是昂验,在AbstractList的幫助下捂敌,很容易實(shí)現(xiàn)作業(yè)的自定義集合。
??技巧是使用冪集中每個(gè)元素的下標(biāo)作為位向量既琴,其中索引中的第n位表示源集中第n個(gè)元素的存在或不存在占婉。本質(zhì)上,0到2n - 1的二進(jìn)制數(shù)與n元素集的冪集之間存在一種自然的映射關(guān)系:
??注意,PowerSet,如果輸入集有30多個(gè)元素甫恩,則會(huì)引發(fā)異常逆济。這突出了使用Collection作為返回類型而不是Stream或Iterable的缺點(diǎn):Collection有一個(gè)int-return size方法,它將返回序列的長(zhǎng)度限制為Integer.MAX_VALUE磺箕,或者2^31 - 1奖慌。集合規(guī)范允許size方法返回2^31 - 1(如果集合更大,甚至是無窮大)松靡,但這不是一個(gè)完全令人滿意的解決方案简僧。
??為了在AbstractCollection之上編寫集合實(shí)現(xiàn),除了Iterable所需的方法之外雕欺,您只需要實(shí)現(xiàn)兩個(gè)方法:contains和size岛马。通常很容易編寫這些方法的有效實(shí)現(xiàn)棉姐。如果這是不可行的,可能是因?yàn)樾蛄械膬?nèi)容在迭代發(fā)生之前沒有預(yù)先確定啦逆,那么返回一個(gè)流或iterable谅海,無論哪個(gè)更自然。如果選擇蹦浦,可以使用兩個(gè)單獨(dú)的方法返回扭吁。
??有時(shí)候,您只會(huì)根據(jù)實(shí)現(xiàn)的簡(jiǎn)單程度來選擇返回類型盲镶。例如侥袜,假設(shè)您想編寫一個(gè)方法來返回輸入列表的所有(連續(xù)的)子列表。只需要三行代碼就可以生成這些子列表并將它們放入一個(gè)標(biāo)準(zhǔn)集合中溉贿,但是保存這個(gè)集合所需的內(nèi)存是源列表大小的兩倍枫吧。雖然這沒有冪集那么糟糕,冪集是指數(shù)的宇色,但顯然是不可接受的九杂。實(shí)現(xiàn)自定義集合(就像我們?yōu)閜ower集所做的那樣)將會(huì)非常單調(diào)乏味,因?yàn)镴DK缺少一個(gè)骨架迭代器實(shí)現(xiàn)來幫助我們宣蠕。
??然而例隆,實(shí)現(xiàn)一個(gè)輸入列表的所有子列表的流是很簡(jiǎn)單的,盡管它需要一個(gè)小的洞察力抢蚀。讓我們將包含列表第一個(gè)元素的子列表稱為列表的前綴镀层。例如,(a, b, c)的前綴是(a)皿曲, (a, b)和(a, b, c)唱逢。類似地,我們調(diào)用包含最后一個(gè)元素a后綴的子列表屋休,所以(a, b, c)的后綴是(a, b, c) (b, c)和(c).我們的理解是琅催,列表的子列表僅僅是前綴的后綴(或后綴的前綴相同)和空列表晃跺。這一觀察直接導(dǎo)致了一個(gè)清晰焙矛、合理簡(jiǎn)潔的實(shí)現(xiàn):
??注意Stream.concat 方法用于將空列表添加到返回的流中垛吗。還要注意,flatMap方法(第45項(xiàng))用于生成一個(gè)由所有前綴的所有后綴組成的流毅哗。最后听怕,注意,我們通過映射IntStream返回的連續(xù)int值流來生成前綴和后綴 IntStream.range 和IntStream.rangeClosed虑绵。這個(gè)習(xí)慣用法大致相當(dāng)于整數(shù)索引上的標(biāo)準(zhǔn)for循環(huán)尿瞭。因此,我們的子列表實(shí)現(xiàn)在本質(zhì)上類似于明顯的嵌套for循環(huán):
??可以將這個(gè)for循環(huán)直接轉(zhuǎn)換為流翅睛。結(jié)果比我們之前的實(shí)現(xiàn)更簡(jiǎn)潔声搁,但可讀性可能略差黑竞。它在本質(zhì)上類似于項(xiàng)目45中笛卡爾積的streams代碼:
??與前面的for循環(huán)一樣,這段代碼不會(huì)發(fā)出空列表疏旨。為了修復(fù)這個(gè)缺陷很魂,您可以像在前一個(gè)版本中那樣使用concat,或者在rangeClosed調(diào)用中使用(int) Math.signum(start)替換1
??子列表的這兩種流實(shí)現(xiàn)都很好檐涝,但是都需要一些用戶使用流到迭代的適配器遏匆,或者在迭代更自然的地方使用流。流到迭代適配器不僅使客戶機(jī)代碼變得混亂谁榜,而且還將我的機(jī)器上的循環(huán)速度降低了2.3倍幅聘。專門構(gòu)建的集合實(shí)現(xiàn)(這里沒有顯示)要詳細(xì)得多,但是運(yùn)行速度大約是基于流的實(shí)現(xiàn)在我的機(jī)器上運(yùn)行速度的1.4倍窃植。
??總之帝蒿,在編寫返回元素序列的方法時(shí),請(qǐng)記住巷怜,您的一些用戶可能希望將它們作為一個(gè)流來處理葛超,而其他用戶可能希望對(duì)它們進(jìn)行迭代。盡量容納兩組人延塑。如果返回集合是可行的绣张,那么就這樣做。如果集合中已經(jīng)有了元素页畦,或者序列中的元素?cái)?shù)量足夠小胖替,可以創(chuàng)建一個(gè)新的元素,那么返回一個(gè)標(biāo)準(zhǔn)集合豫缨,比如ArrayList。否則端朵,請(qǐng)考慮像我們?yōu)閜ower set所做的那樣實(shí)現(xiàn)自定義集合好芭。如果無法返回集合、返回流或iterable冲呢,則選擇看起來更自然的方法舍败。如果在將來的Java版本中,流接口聲明被修改為擴(kuò)展Iterable敬拓,那么您應(yīng)該可以隨意返回流邻薯,因?yàn)樗鼈兺瑫r(shí)支持流處理和迭代。
本文寫于2019.7.15乘凸,歷時(shí)1天