??streams API是在Java 8中添加的,以簡化按順序或并行執(zhí)行批量操作的任務(wù)杖虾。這個API提供了兩個關(guān)鍵抽象:流表示有限或無限序列的數(shù)據(jù)元素,流管道表示對這些元素的多級計算。流中的元素可以來自任何地方。常見的源包括集合姆涩、數(shù)組、文件涮雷、正則表達式模式匹配器、偽隨機數(shù)生成器和其他流轻局。流中的數(shù)據(jù)元素可以是對象引用或基本值洪鸭。支持三種基本類型:int、long和double仑扑。
??流管道由源流览爵、零個或多個中間操作和一個終端操作組成。每個中間操作都以某種方式轉(zhuǎn)換流镇饮,例如將每個元素映射到該元素的函數(shù)蜓竹,或者過濾掉不滿足某些條件的所有元素。中間操作都將一個流轉(zhuǎn)換為另一個流储藐,其元素類型可能與輸入流相同俱济,也可能與輸入流不同。終端操作對最后一個中間操作產(chǎn)生的流執(zhí)行最后一次計算钙勃,例如將其元素存儲到集合中蛛碌、返回某個元素或打印其所有元素。
??流管道的計算是延遲的:直到調(diào)用終端操作才開始計算辖源,并且永遠不會計算完成終端操作所需的數(shù)據(jù)元素蔚携。這種延遲計算使處理無限流成為可能希太。注意,沒有終端操作的流管道是靜默的no-op酝蜒,所以不要忘記包含一個誊辉。
??streams API是連貫的:它的設(shè)計允許將包含管道的所有調(diào)用都鏈接到一個表達式中。事實上亡脑,可以將多個管道鏈接到一個表達式中
??默認情況下堕澄,流管道按順序運行。使管道并行執(zhí)行與在管道中的任何流上調(diào)用并行方法一樣簡單远豺,但是很少適合這樣做(item48 ).
??streams API具有足夠的通用性奈偏,實際上任何計算都可以使用streams執(zhí)行,但不能因為可以就意味著應(yīng)該這樣做躯护。如果使用得當惊来,流可以使程序更短、更清晰;如果使用不當棺滞,它們會使程序難以閱讀和維護裁蚁,對于何時使用流沒有硬性的規(guī)則,但是有啟發(fā)式.
??考慮下面的程序继准,它從字典文件中讀取單詞枉证,并打印大小滿足用戶指定的最小值的所有字謎組.記住,如果兩個單詞由不同的字母組成移必,那么它們就是字謎順序.程序從用戶指定的字典文件中讀取每個單詞室谚,并將這些單詞放入映射中,map key是按字母順序排列的單詞,所以“staple”的key是“aelpst”崔泵,“petals”的key也是“aelpst”:staple.這兩個單詞是字謎秒赤,所有的字謎都有相同的字母排列形式(有時也稱為字母組合)。map值是一個列表憎瘸,其中包含所有共享字母格式的單詞.在字典被處理之后入篮,每個列表都是一個完整的字謎組.然后,程序遍歷map的values()視圖幌甘,并打印大小滿足閾值的每個列表:
??這個計劃中的一個步驟值得注意.將每個單詞插入映射(以粗體顯示)使用computeIfAbsent方法潮售,該方法是在Java 8中添加的,此方法在映射中查找鍵:如果鍵存在,則該方法只返回與其關(guān)聯(lián)的值锅风。如果沒有酥诽,該方法將給定的函數(shù)對象應(yīng)用于鍵來計算值,將該值與鍵關(guān)聯(lián)皱埠,并返回計算值.computeIfAbsent方法簡化了將多個值與每個鍵關(guān)聯(lián)的映射的實現(xiàn)
??現(xiàn)在考慮下面的程序盆均,它解決了同樣的問題,但是大量使用了流.注意整個程序,除了打開字典文件引發(fā)異常的代碼,它只包含了單一表達式.字典以單獨的表達式打開的惟一原因是允許使用try-with-resources語句漱逸,該語句確保字典文件是關(guān)閉的.
??如果您發(fā)現(xiàn)這段代碼很難讀泪姨,不要擔(dān)心;你不是一個人.它更短游沿,但是可讀性也更差,特別是對于那些不擅長使用流的程序員來說. 過度使用流使得程序難以閱讀和維護.
??幸運的是肮砾,有一個折中的辦法.下面的程序解決了相同的問題诀黍,在不過度使用流的情況下使用流.結(jié)果是一個程序,比原來的更短仗处,更清晰:
??即使您以前很少接觸流眯勾,這個程序也不難理解.它在try-with-resources塊中打開字典文件,獲得由文件中的所有行組成的流.流變量名為words婆誓,表示流中的每個元素都是一個單詞.該流上的管道沒有中間操作;它的終端操作將所有單詞收集到一個地圖中吃环,然后按字母順序?qū)卧~進行分組(item46 ).這與在程序的前兩個版本中構(gòu)造的映射完全相同.然后在map的values()視圖上打開一個新的流<List<String>>.當然,這個流中的元素是字謎組.對流進行過濾洋幻,以便忽略大小小于minGroupSize的所有組郁轻,最后,通過終端操作forEach打印剩余的組.
??注意文留,lambda參數(shù)名是經(jīng)過仔細選擇的.參數(shù)g實際上應(yīng)該被命名為group好唯,但是生成的代碼行對于本書來說太寬了.在沒有顯式類型的情況下,仔細命名lambda參數(shù)對于流管道的可讀性至關(guān)重要.
??還要注意燥翅,單詞的字母化是在單獨的alphabetize 方法中完成的.通過為操作提供名稱并將實現(xiàn)細節(jié)排除在主程序之外骑篙,這增強了可讀性.對于流管道中的可讀性,使用helper方法甚至比在迭代代碼中更重要.因為管道缺少顯式類型信息和命名的臨時變量.
??可以重新實現(xiàn)alphabetize方法來使用流森书,但是基于流的alphabetize方法不太清晰靶端,更難于正確地編寫,而且速度可能更慢凛膏。這些缺陷是由于Java缺乏對原始char流的支持(這并不意味著Java應(yīng)該支持char流;這樣做是不可能的)杨名。要演示使用流處理char值的危害,請考慮以下代碼:
"Hello world!".chars().forEach(System.out::print);
??您可能期望它打印Hello world!译柏,但是如果運行它镣煮,您會發(fā)現(xiàn)它打印了721011081081113211911111410810033,這是因為“Hello world!”.chars()返回的流的元素不是char值姐霍,而是int值鄙麦,因此調(diào)用了print的int重載.一個名為chars的方法返回一個int值流,這確實令人困惑.您可以通過使用強制轉(zhuǎn)換強制調(diào)用正確的重載來修復(fù)程序:
"Hello world!".chars().forEach(x -> System.out.print((char) x));
但是理想情況下镊折,您應(yīng)該避免使用流來處理char值胯府。
??當您開始使用流時,您可能會有將所有循環(huán)轉(zhuǎn)換為流的沖動恨胚,但是要抵制這種沖動.雖然這是可能的骂因,但它可能會損害代碼庫的可讀性和可維護性.通常,即使是中等復(fù)雜的任務(wù)赃泡,也最好使用流和迭代的組合來完成寒波,如上面的字謎程序所示.因此乘盼,重構(gòu)現(xiàn)有代碼以使用流,并僅在有意義的地方在新代碼中使用它們.
??- 從代碼塊中俄烁,您可以讀取或修改范圍內(nèi)的任何局部變量;從lambda中绸栅,您只能讀取final或有效的final變量[JLS 4.12.4],并且不能修改任何局部變量页屠。
-
從代碼塊中粹胯,可以從封閉方法返回、中斷或繼續(xù)封閉循環(huán)辰企,或者拋出聲明該方法要拋出的任何已檢查異常;對于labmda风纠,你什么都不能做。
如果使用這些技術(shù)最好地表達計算牢贸,那么它可能不適合流竹观。相反,流使做一些事情變得非常容易:
一致變換元素序列
篩選元素的順序
使用單個操作組合元素序列(例如添加十减、連接或計算它們的最小值)
將元素序列累積到一個集合中栈幸,可能根據(jù)某個公共屬性對它們進行分組
-
搜索元素序列,尋找滿足某種條件的元素
如果計算是用這些技術(shù)最好地表達的帮辟,那么它是流的一個很好的候選.
??使用流很難做的一件事是同時訪問來自管道的多個階段的相應(yīng)元素:一旦將一個值映射到其他值速址,原始值就會丟失.一種解決方法是將每個值映射到包含原始值和新值的pair對象,但這不是一個令人滿意的解決方案由驹,尤其是在管道的多個階段需要pair對象時.生成的代碼混亂而冗長芍锚,這違背了流的主要目的.它適用時,更好的解決方法是在需要訪問早期值時反轉(zhuǎn)映射.
??例如蔓榄,讓我們編寫一個程序來打印前20個梅森素數(shù)并炮。為了提醒你,梅森數(shù)是2^p - 1形式的數(shù)甥郑。如果p是質(zhì)數(shù)逃魄,對應(yīng)的梅森數(shù)可能是質(zhì)數(shù);如果是,那就是梅森素數(shù).下面是返回(無限)流的方法澜搅。我們假設(shè)一個靜態(tài)導(dǎo)入被用來方便地訪問BigInteger的靜態(tài)成員:
image.png
??方法的名稱(primes)是描述流元素的復(fù)數(shù)名詞.對于所有返回流的方法伍俘,強烈推薦使用這種命名約定,因為它增強了流管道的可讀性.該方法使用靜態(tài)工廠流勉躺。iterate癌瘾,它接受兩個參數(shù):流中的第一個元素,以及從前一個元素生成流中的下一個元素的函數(shù)饵溅。下面是打印前20個梅森素數(shù)的程序:
??這個程序?qū)ι厦娴纳⑽拿枋鲞M行了簡單的編碼:它從質(zhì)數(shù)開始妨退,計算相應(yīng)的梅森數(shù),過濾除質(zhì)數(shù)之外的所有數(shù)(魔法值50控制概率素數(shù)測試),將結(jié)果流限制為20個元素咬荷,并將它們打印出來冠句。
??現(xiàn)在假設(shè)我們想在每個Mersenne '之前加上它的指數(shù)(p)。這個值只出現(xiàn)在初始流中幸乒,所以在輸出結(jié)果的終端操作中是不可訪問的轩端。幸運的是,通過反轉(zhuǎn)第一個中間操作中發(fā)生的映射逝变,可以很容易地計算梅森數(shù)的指數(shù)基茵。指數(shù)只是二進制表示中的比特數(shù),所以這個終端操作產(chǎn)生了想要的結(jié)果:
??在許多任務(wù)中壳影,是否使用流或迭代并不明顯.例如拱层,考慮初始化一副新紙牌的任務(wù).假設(shè)Card是一個不可變的值類,它封裝了一個Rank和一個Suit宴咧,它們都是enum類型.這個任務(wù)代表任何任務(wù)要求計算可從兩個集合中選擇的所有元素對.數(shù)學(xué)家把這叫做兩個集合的笛卡爾積.這里是一個迭代實現(xiàn)嵌套的for-each循環(huán)根灯,你應(yīng)該非常熟悉:
??這里是一個基于流的實現(xiàn),它使用了中間操作flatMap掺栅。該操作將流中的每個元素映射到一個流烙肺,然后將所有這些新流連接到一個流中(或?qū)⑺鼈儔罕?。注意氧卧,這個實現(xiàn)包含一個嵌套的lambda桃笙,用粗體顯示:
??兩個版本的newDeck中哪個更好?這可以歸結(jié)為個人偏好和編程環(huán)境。第一個版本更簡單沙绝,可能感覺更自然搏明。大部分Java程序員將能夠理解并維護它,但是有些程序員對第二個(基于流的)版本會感到更舒服闪檬。如果您對流和函數(shù)式編程相當精通星著,那么它會更簡潔一些,也不會太難理解.如果您不確定您更喜歡哪個版本粗悯,迭代版本可能是更安全的選擇,如果您更喜歡流版本虚循,并且您相信其他使用該代碼的程序員也會與您有相同的偏好,那么您應(yīng)該使用它样傍。
??總之横缔,有些任務(wù)最好使用流來完成,有些任務(wù)最好使用迭代來完成铭乾。許多任務(wù)最好通過結(jié)合這兩種方法來完成剪廉。對于選擇用于任務(wù)的方法沒有硬性的規(guī)則娃循,但是有一些有用的啟發(fā)式.在許多情況下炕檩,使用哪種方法是很清楚的;在某些情況下,它不會. 如果您不確定流或迭代是否更好地服務(wù)于任務(wù),請同時嘗試這兩種方法笛质,看看哪種效果更好泉沾。
本文寫于2019.7.12,歷時3天