Effective Java(3rd)-Item45 謹慎使用stream

??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()視圖幌甘,并打印大小滿足閾值的每個列表:

image.png

??這個計劃中的一個步驟值得注意.將每個單詞插入映射(以粗體顯示)使用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)閉的.


image.png

??如果您發(fā)現(xiàn)這段代碼很難讀泪姨,不要擔(dān)心;你不是一個人.它更短游沿,但是可讀性也更差,特別是對于那些不擅長使用流的程序員來說. 過度使用流使得程序難以閱讀和維護.
??幸運的是肮砾,有一個折中的辦法.下面的程序解決了相同的問題诀黍,在不過度使用流的情況下使用流.結(jié)果是一個程序,比原來的更短仗处,更清晰:

image.png

??即使您以前很少接觸流眯勾,這個程序也不難理解.它在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ù)的程序:


image.png

??這個程序?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é)果:


image.png

??在許多任務(wù)中壳影,是否使用流或迭代并不明顯.例如拱层,考慮初始化一副新紙牌的任務(wù).假設(shè)Card是一個不可變的值類,它封裝了一個Rank和一個Suit宴咧,它們都是enum類型.這個任務(wù)代表任何任務(wù)要求計算可從兩個集合中選擇的所有元素對.數(shù)學(xué)家把這叫做兩個集合的笛卡爾積.這里是一個迭代實現(xiàn)嵌套的for-each循環(huán)根灯,你應(yīng)該非常熟悉:


image.png

??這里是一個基于流的實現(xiàn),它使用了中間操作flatMap掺栅。該操作將流中的每個元素映射到一個流烙肺,然后將所有這些新流連接到一個流中(或?qū)⑺鼈儔罕?。注意氧卧,這個實現(xiàn)包含一個嵌套的lambda桃笙,用粗體顯示:


image.png

??兩個版本的newDeck中哪個更好?這可以歸結(jié)為個人偏好和編程環(huán)境。第一個版本更簡單沙绝,可能感覺更自然搏明。大部分Java程序員將能夠理解并維護它,但是有些程序員對第二個(基于流的)版本會感到更舒服闪檬。如果您對流和函數(shù)式編程相當精通星著,那么它會更簡潔一些,也不會太難理解.如果您不確定您更喜歡哪個版本粗悯,迭代版本可能是更安全的選擇,如果您更喜歡流版本虚循,并且您相信其他使用該代碼的程序員也會與您有相同的偏好,那么您應(yīng)該使用它样傍。
??總之横缔,有些任務(wù)最好使用流來完成,有些任務(wù)最好使用迭代來完成铭乾。許多任務(wù)最好通過結(jié)合這兩種方法來完成剪廉。對于選擇用于任務(wù)的方法沒有硬性的規(guī)則娃循,但是有一些有用的啟發(fā)式.在許多情況下炕檩,使用哪種方法是很清楚的;在某些情況下,它不會. 如果您不確定流或迭代是否更好地服務(wù)于任務(wù),請同時嘗試這兩種方法笛质,看看哪種效果更好泉沾。

本文寫于2019.7.12,歷時3天

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妇押,一起剝皮案震驚了整個濱河市跷究,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敲霍,老刑警劉巖俊马,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異肩杈,居然都是意外死亡柴我,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門扩然,熙熙樓的掌柜王于貴愁眉苦臉地迎上來艘儒,“玉大人,你說我怎么就攤上這事夫偶〗缯觯” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵兵拢,是天一觀的道長翻斟。 經(jīng)常有香客問我,道長说铃,這世上最難降的妖魔是什么杨赤? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮截汪,結(jié)果婚禮上疾牲,老公的妹妹穿的比我還像新娘。我一直安慰自己衙解,他們只是感情好阳柔,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚓峦,像睡著了一般舌剂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上暑椰,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天霍转,我揣著相機與錄音,去河邊找鬼一汽。 笑死避消,一個胖子當著我的面吹牛低滩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播岩喷,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼恕沫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纱意?” 一聲冷哼從身側(cè)響起婶溯,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎偷霉,沒想到半個月后迄委,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡类少,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年跑筝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞒滴。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡曲梗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妓忍,到底是詐尸還是另有隱情虏两,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布世剖,位于F島的核電站定罢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏旁瘫。R本人自食惡果不足惜祖凫,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酬凳。 院中可真熱鬧惠况,春花似錦、人聲如沸宁仔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翎苫。三九已至权埠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間煎谍,已是汗流浹背攘蔽。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呐粘,地道東北人满俗。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓转捕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親漫雷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容