日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

《Java8实战》笔记(13):函数式的思考

發(fā)布時(shí)間:2023/12/13 java 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《Java8实战》笔记(13):函数式的思考 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

函數(shù)式的思考

實(shí)現(xiàn)和維護(hù)系統(tǒng)

為了讓程序易于使用,你還希望它具備哪些特性呢?

  • 你會(huì)希望它具有良好的結(jié)構(gòu),最好類的結(jié)構(gòu)應(yīng)該反映出系統(tǒng)的結(jié)構(gòu),這樣能便于大家理解;
  • 甚至軟件工程中還提供了指標(biāo),對(duì)結(jié)構(gòu)的合理性進(jìn)行評(píng)估,比如耦合性(軟件系統(tǒng)中各組件之間是否相互獨(dú)立)以及內(nèi)聚性(系統(tǒng)的各相關(guān)部分之間如何協(xié)作)。
  • 對(duì)大多數(shù)程序員而言,最關(guān)心的日常要?jiǎng)?wù)是代碼維護(hù)時(shí)的調(diào)試:代碼遭遇一些無(wú)法預(yù)期的值就有可能發(fā)生崩潰。

    • 為什么會(huì)發(fā)生這種情況?
    • 它是如何進(jìn)入到這種狀態(tài)的?

    想想看你有多少代碼維護(hù)的顧慮都能歸咎到這一類!很明顯,函數(shù)式編程提出的“無(wú)副作用”以及“不變性”對(duì)于解決這一難題是大有裨益的。

    共享的可變數(shù)據(jù)

    無(wú)法預(yù)知的變量修改問(wèn)題,都源于共享的數(shù)據(jù)結(jié)構(gòu)被你所維護(hù)的代碼中的多個(gè)方法讀取和更新。

    假設(shè)幾個(gè)類同時(shí)都保存了指向某個(gè)列表的引用。

    • 那么到底誰(shuí)對(duì)這個(gè)列表?yè)碛兴鶎贆?quán)呢?
    • 如果一個(gè)類對(duì)它進(jìn)行了修改,會(huì)發(fā)生什么情況?
    • 其他的類預(yù)期會(huì)發(fā)生這種變化嗎?
    • 其他的類又如何得知列表發(fā)生了修改呢?
    • 我們需要通知使用該列表的所有類這一變化嗎?
    • 抑或是不是每個(gè)類都應(yīng)該為自己準(zhǔn)備一份防御式的數(shù)據(jù)備份以備不時(shí)之需呢?

    換句話說(shuō),由于使用了可變的共享數(shù)據(jù)結(jié)構(gòu),我們很難追蹤你程序的各個(gè)組成部分所發(fā)生的變化。

    假設(shè)有這樣一個(gè)系統(tǒng),它不修改任何數(shù)據(jù)。維護(hù)這樣的一個(gè)系統(tǒng)將是一個(gè)無(wú)以倫比的美夢(mèng),因?yàn)槟悴辉贂?huì)收到任何由于某些對(duì)象在某些地方修改了某個(gè)數(shù)據(jù)結(jié)構(gòu)而導(dǎo)致的意外報(bào)告。如果一個(gè)方法既不修改它內(nèi)嵌類的狀態(tài),也不修改其他對(duì)象的狀態(tài),使用return返回所有的計(jì)算結(jié)果,那么我們稱其為純粹的或者無(wú)副作用的。

    更確切地講,到底哪些因素會(huì)造成副作用呢?簡(jiǎn)而言之,副作用就是函數(shù)的效果已經(jīng)超出了函數(shù)自身的范疇。下面是一些例子。

    • 除了構(gòu)造器內(nèi)的初始化操作,對(duì)類中數(shù)據(jù)結(jié)構(gòu)的任何修改,包括字段的賦值操作(一個(gè)典型的例子是setter方法)。
    • 拋出一個(gè)異常。
    • 進(jìn)行輸入/輸出操作,比如向一個(gè)文件寫數(shù)據(jù)。

    從另一個(gè)角度來(lái)看“無(wú)副作用”的話,我們就應(yīng)該考慮不可變對(duì)象。不可變對(duì)象是這樣一種對(duì)象,它們一旦完成初始化就不會(huì)被任何方法修改狀態(tài)。這意味著一旦一個(gè)不可變對(duì)象初始化完畢,它永遠(yuǎn)不會(huì)進(jìn)入到一個(gè)無(wú)法預(yù)期的狀態(tài)。你可以放心地共享它,無(wú)需保留任何副本,并且由于它們不會(huì)被修改,還是線程安全的。

    如果構(gòu)成系統(tǒng)的各個(gè)組件都能遵守這一原則,該系統(tǒng)就能在完全無(wú)鎖的情況下,使用多核的并發(fā)機(jī)制,因?yàn)槿魏我粋€(gè)方法都不會(huì)對(duì)其他的方法造成干擾。

    聲明式編程

    一般通過(guò)編程實(shí)現(xiàn)一個(gè)系統(tǒng),有兩種思考方式。

    專注于如何實(shí)現(xiàn)

    How to do

    一種專注于如何實(shí)現(xiàn),比如:“首先做這個(gè),緊接著更新那個(gè),然后……”

    舉個(gè)例子,如果你希望通過(guò)計(jì)算找出列表中最昂貴的事務(wù),通常需要執(zhí)行一系列的命令:

    • 從列表中取出一個(gè)事務(wù),將其與臨時(shí)最昂貴事務(wù)進(jìn)行比較;
    • 如果該事務(wù)開銷更大,就將臨時(shí)最昂貴的事務(wù)設(shè)置為該事務(wù);
    • 接著從列表中取出下一個(gè)事務(wù),并重復(fù)上述操作。

    這種“如何做”風(fēng)格的編程非常適合經(jīng)典的面向?qū)ο缶幊?#xff0c;有些時(shí)候我們也稱之為“命令式”,因?yàn)樗奶攸c(diǎn)是它的指令和計(jì)算機(jī)底層的詞匯非常相近,比如賦值、條件分支以及循環(huán),就像下面這段代碼:

    Transaction mostExpensive = transactions.get(0); if(mostExpensive == null)throw new IllegalArgumentException("Empty list of transactions") for(Transaction t: transactions.subList(1, transactions.size())){if(t.getValue() > mostExpensive.getValue()){mostExpensive = t;} }

    關(guān)注要做什么

    what to do

    另一種方式則更加關(guān)注要做什么。使用Stream API你可以指定下面這樣的查詢:

    Optional<Transaction> mostExpensive = transactions.stream().max(comparing(Transaction::getValue));

    這個(gè)查詢把最終如何實(shí)現(xiàn)的細(xì)節(jié)留給了函數(shù)庫(kù)。我們把這種思想稱之為內(nèi)部迭代。它的巨大優(yōu)勢(shì)在于你的查詢語(yǔ)句現(xiàn)在讀起來(lái)就像是問(wèn)題陳述,由于采用了這種方式,比理解一系列的命令要簡(jiǎn)潔得多。

    采用這種“要做什么”風(fēng)格的編程通常被稱為聲明式編程。你制定規(guī)則,給出了希望實(shí)現(xiàn)的目標(biāo),讓系統(tǒng)來(lái)決定如何實(shí)現(xiàn)這個(gè)目標(biāo)。它帶來(lái)的好處非常明顯,用這種方式編寫的代碼更加接近問(wèn)題陳述了。

    為什么要采用函數(shù)式編程

    函數(shù)式編程具體實(shí)踐了聲明式編程(“你只需要使用不相互影響的表達(dá)式,描述想要做什么,由系統(tǒng)來(lái)選擇如何實(shí)現(xiàn)”)和無(wú)副作用計(jì)算,這兩個(gè)思想能幫助你更容易地構(gòu)建和維護(hù)系統(tǒng)。

    一些語(yǔ)言的特性,比如構(gòu)造操作和傳遞行為對(duì)于以自然的方式實(shí)現(xiàn)聲明式編程是必要的,它們能讓我們的程序更便于閱讀,易于編寫。你可以使用Stream將幾個(gè)操作串接在一起,表達(dá)一個(gè)復(fù)雜的查詢。這些都是函數(shù)式編程語(yǔ)言的特性

    什么是函數(shù)式編程

    對(duì)于“什么是函數(shù)式編程”這一問(wèn)題最簡(jiǎn)化的回答是“它是一種使用函數(shù)進(jìn)行編程的方式”。那什么是函數(shù)呢?

    很容易想象這樣一個(gè)方法,它接受一個(gè)整型和一個(gè)浮點(diǎn)型參數(shù),返回一個(gè)浮點(diǎn)型的結(jié)果——它也有副作用,隨著調(diào)用次數(shù)的增加,它會(huì)不斷地更新共享變量,如下圖所示。

    在函數(shù)式編程的上下文中,一個(gè)“函數(shù)”對(duì)應(yīng)于一個(gè)數(shù)學(xué)函數(shù):它接受零個(gè)或多個(gè)參數(shù),生成一個(gè)或多個(gè)結(jié)果,并且不會(huì)有任何副作用。你可以把它看成一個(gè)黑盒,它接收輸入并產(chǎn)生一些輸出

    這種類型的函數(shù)和你在Java編程語(yǔ)言中見(jiàn)到的函數(shù)之間的區(qū)別是非常重要的(我們無(wú)法想象,log或者 sin這樣的數(shù)學(xué)函數(shù)會(huì)有副作用)。尤其是,使用同樣的參數(shù)調(diào)用數(shù)學(xué)函數(shù),它所返回的結(jié)果一定是相同的。


    當(dāng)談?wù)摗昂瘮?shù)式”時(shí),我們想說(shuō)的其實(shí)是“像數(shù)學(xué)函數(shù)那樣——沒(méi)有副作用”。由此,編程上的一些精妙問(wèn)題隨之而來(lái)。我們的意思是,每個(gè)函數(shù)都只能使用函數(shù)和像if-then-else這樣的數(shù)學(xué)思想來(lái)構(gòu)建嗎?

    或者,我們也允許函數(shù)內(nèi)部執(zhí)行一些非函數(shù)式的操作,只要這些操作的結(jié)果不會(huì)暴露給系統(tǒng)中的其他部分?換句話說(shuō),如果程序有一定的副作用,不過(guò)該副作用不會(huì)為其他的調(diào)用者感知,是否我們能假設(shè)這種副作用不存在呢?調(diào)用者不需要知道,或者完全不在意這些副作用,因?yàn)檫@對(duì)它完全沒(méi)有影響。

    當(dāng)我們希望能界定這二者之間的區(qū)別時(shí),我們將第一種稱為純粹的函數(shù)式編程,后者稱為函數(shù)式編程。

    函數(shù)式Java編程

    編程實(shí)戰(zhàn)中,你是無(wú)法用Java語(yǔ)言以純粹的函數(shù)式來(lái)完成一個(gè)程序的。

    比如,Java的I/O模型就包含了帶副作用的方法(調(diào)用Scanner.nextLine就有副作用,它會(huì)從一個(gè)文件中讀取一行,通常情況兩次調(diào)用的結(jié)果完全不同)。

    不過(guò),你還是有可能為你系統(tǒng)的核心組件編寫接近純粹函數(shù)式的實(shí)現(xiàn)。在Java語(yǔ)言中,如果你希望編寫函數(shù)式的程序,首先需要做的是確保沒(méi)有人能覺(jué)察到你代碼的副作用,這也是函數(shù)式的含義。假設(shè)這樣一個(gè)函數(shù)或者方法,它沒(méi)有副作用,進(jìn)入方法體執(zhí)行時(shí)會(huì)對(duì)一個(gè)字段的值加一,退出方法體之前會(huì)對(duì)該字段減一。對(duì)一個(gè)單線程的程序而言,這個(gè)方法是沒(méi)有副作用的,可以看作函數(shù)式的實(shí)現(xiàn)。

    換個(gè)角度而言,如果另一個(gè)線程可以查看該字段的值——或者更糟糕的情況,該方法會(huì)同時(shí)被多個(gè)線程并發(fā)調(diào)用——那么這個(gè)方法就不能稱之為函數(shù)式的實(shí)現(xiàn)了。

    當(dāng)然,你可以用加鎖的方式對(duì)方法的方法體進(jìn)行封裝,掩蓋這一問(wèn)題,你甚至可以再次聲稱該方法符合函數(shù)式的約定。但是,這樣做之后,你就失去了在你的多核處理器的兩個(gè)核上并發(fā)執(zhí)行兩個(gè)方法調(diào)用的能力。它的副作用對(duì)程序可能是不可見(jiàn)的,不過(guò)對(duì)于程序員你而言是可見(jiàn)的,因?yàn)槌绦蜻\(yùn)行的速度變慢了!

    我們的準(zhǔn)則是,被稱為“函數(shù)式”的函數(shù)或方法都只能修改本地變量。除此之外,它引用的對(duì)象都應(yīng)該是不可修改的對(duì)象。通過(guò)這種規(guī)定,我們期望所有的字段都為final類型,所有的引用類型字段都指向不可變對(duì)象。后續(xù)的內(nèi)容中,你會(huì)看到我們實(shí)際也允許對(duì)方法中全新創(chuàng)建的對(duì)象中的字段進(jìn)行更新,不過(guò)這些字段對(duì)于其他對(duì)象都是不可見(jiàn)的,也不會(huì)因?yàn)楸4鎸?duì)后續(xù)調(diào)用結(jié)
    果造成影響。


    我們前述的準(zhǔn)則是不完備的,要成為真正的函數(shù)式程序還有一個(gè)附加條件,不過(guò)它在最初時(shí)不太為大家所重視。要被稱為函數(shù)式,函數(shù)或者方法不應(yīng)該拋出任何異常。關(guān)于這一點(diǎn),有一個(gè)極為簡(jiǎn)單而又極為教條的解釋:你不應(yīng)該拋出異常,因?yàn)橐坏伋霎惓?#xff0c;就意味著結(jié)果被終止了;不再像我們之前討論的黑盒模式那樣,由return返回一個(gè)恰當(dāng)?shù)慕Y(jié)果值。

    不過(guò),這一規(guī)則似乎又和我們實(shí)際的數(shù)學(xué)使用有沖突:雖然合法的數(shù)學(xué)函數(shù)為每個(gè)合法的參數(shù)值返回一個(gè)確定的結(jié)果,很多通用的數(shù)學(xué)操作在嚴(yán)格意義上稱之為局部函數(shù)式(partial function)可能更為妥當(dāng)。這種函數(shù)對(duì)于某些輸入值,甚至是大多數(shù)的輸入值都返回一個(gè)確定的結(jié)果;不過(guò)對(duì)另一些輸入值,它的結(jié)果是未定義的,甚至不返回任何結(jié)果。

    這其中一個(gè)典型的例子是除法和開平方運(yùn)算,如果除法的第二操作數(shù)是0,或者開平方的參數(shù)為負(fù)數(shù)就會(huì)發(fā)生這樣的情況。以Java那樣拋出一個(gè)異常的方式對(duì)這些情況進(jìn)行建??雌饋?lái)非常自然。這里存在著一定的爭(zhēng)執(zhí),有的作者認(rèn)為拋出代表嚴(yán)重錯(cuò)誤的異常是可以接受的,但是捕獲異常是一種非函數(shù)式的控制流,因?yàn)檫@種操作違背了我們?cè)诤诤心P椭卸x的“傳遞參數(shù),返回結(jié)果”的規(guī)則,引出了代表異常處理的第三支箭頭,如下圖所示。

    那么,如果不使用異常,你該如何對(duì)除法這樣的函數(shù)進(jìn)行建模呢?答案是請(qǐng)使用Optional<T>類型:你應(yīng)該避免讓sqrt使用double sqrt(double)這樣的函數(shù)簽名,因?yàn)檫@種方式可能拋出異常;與之相反我們推薦你使用Optional<Double> sqrt(double)——這種方式下,函數(shù)要么返回一個(gè)值表示調(diào)用成功,要么返回一個(gè)對(duì)象,表明其無(wú)法進(jìn)行指定的操作。

    當(dāng)然,這意味著調(diào)用者需要檢查方法返回的是否為一個(gè)空的Optional對(duì)象。這件事聽起來(lái)代價(jià)不小,依據(jù)我們之前對(duì)函數(shù)式編程和純粹的函數(shù)式編程的比較,從實(shí)際操作的角度出發(fā),你可以選擇在本地局部地使用異常,避免通過(guò)接口將結(jié)果暴露給其他方法,這種方式既取得了函數(shù)式的優(yōu)點(diǎn),又不會(huì)過(guò)度膨脹代碼。

    最后,作為函數(shù)式的程序,你的函數(shù)或方法調(diào)用的庫(kù)函數(shù)如果有副作用,你必須設(shè)法隱藏它們的非函數(shù)式行為,否則就不能調(diào)用這些方法(換句話說(shuō),你需要確保它們對(duì)數(shù)據(jù)結(jié)構(gòu)的任何修改對(duì)于調(diào)用者都是不可見(jiàn)的,你可以通過(guò)首次復(fù)制,或者捕獲任何可能拋出的異常實(shí)現(xiàn)這一目的)

    引用透明性

    “沒(méi)有可感知的副作用”(不改變對(duì)調(diào)用者可見(jiàn)的變量、不進(jìn)行I/O、不拋出異常)的這些限制都隱含著引用透明性。如果一個(gè)函數(shù)只要傳遞同樣的參數(shù)值,總是返回同樣的結(jié)果,那這個(gè)函數(shù)就是引用透明的。

    String.replace方法就是引用透明的,因?yàn)橄?#34;raoul".replace(‘r’,‘R’)這樣的調(diào)用總是返回同樣的結(jié)果(replace方法返回一個(gè)新的字符串,用小寫的r替換掉所有大寫的R),而不是更新它的this對(duì)象,所以它可以被看成函數(shù)式的。

    換句話說(shuō),函數(shù)無(wú)論在何處、何時(shí)調(diào)用,如果使用同樣的輸入總能持續(xù)地得到相同的結(jié)果,就具備了函數(shù)式的特征。

    這也解釋了我們?yōu)槭裁床话裄andom.nextInt看成函數(shù)式的方法。Java語(yǔ)言中,使用Scanner對(duì)象從用戶的鍵盤讀取輸入也違反了引用透明性原則,因?yàn)槊看握{(diào)用nextLine時(shí)都可能得到不同的結(jié)果。不過(guò),將兩個(gè)final int類型的變量相加總能得到同樣的結(jié)果,因?yàn)樵谶@種聲明方式下,變量的內(nèi)容是不會(huì)被改變的。


    引用透明性是理解程序的一個(gè)重要屬性。它還包含了對(duì)代價(jià)昂貴或者需長(zhǎng)時(shí)間計(jì)算才能得到結(jié)果的變量值的優(yōu)化(通過(guò)保存機(jī)制而不是重復(fù)計(jì)算),我們通常將其稱為記憶化或者緩存

    Java語(yǔ)言中,關(guān)于引用透明性還有一個(gè)比較復(fù)雜的問(wèn)題。假設(shè)你對(duì)一個(gè)返回列表的方法調(diào)用了兩次。這兩次調(diào)用會(huì)返回內(nèi)存中的兩個(gè)不同列表,不過(guò)它們包含了相同的元素。如果這些列表被當(dāng)作可變的對(duì)象值(因此是不相同的),那么該方法就不是引用透明的。如果你計(jì)劃將這些列表作為單純的值(不可修改),那么把這些值看成相同的是合理的,這種情況下該方法是引用透
    明的。通常情況下,在函數(shù)式編程中,你應(yīng)該選擇使用引用透明的函數(shù)。

    面向?qū)ο蟮木幊毯秃瘮?shù)式編程的對(duì)比

    我們由函數(shù)式編程和(極端)典型的面向?qū)ο缶幊痰膶?duì)比入手進(jìn)行介紹,最終你會(huì)發(fā)現(xiàn)Java8認(rèn)為這些風(fēng)格其實(shí)只是面向?qū)ο蟮囊粋€(gè)極端。作為Java程序員,毫無(wú)疑問(wèn),你一定使用過(guò)某種函數(shù)式編程,也一定使用過(guò)某些我們稱為極端面向?qū)ο蟮木幊?。由于硬?#xff08;比如多核)和程序員期望(比如使用類數(shù)據(jù)庫(kù)查詢式的語(yǔ)言去操縱數(shù)據(jù))的變化,促使Java的軟件工程風(fēng)格在某種程度上愈來(lái)愈向函數(shù)式的方向傾斜。

    關(guān)于這個(gè)問(wèn)題有兩種觀點(diǎn)。

  • 一種支持極端的面向?qū)ο?#xff1a;任何事物都是對(duì)象,程序要么通過(guò)更新字段完成操作,要么調(diào)用對(duì)與它相關(guān)的對(duì)象進(jìn)行更新的方法。
  • 另一種觀點(diǎn)支持引用透明的函數(shù)式編程,認(rèn)為方法不應(yīng)該有(對(duì)外部可見(jiàn)的)對(duì)象修改。
  • 實(shí)際操作中,Java程序員經(jīng)?;煊眠@些風(fēng)格。你可能會(huì)使用包含了可變內(nèi)部狀態(tài)的迭代器遍歷某個(gè)數(shù)據(jù)結(jié)構(gòu),同時(shí)又通過(guò)函數(shù)式的方式計(jì)算數(shù)據(jù)結(jié)構(gòu)中的變量之和。

    函數(shù)式編程實(shí)戰(zhàn)

    SubsetsMain

    一個(gè)示例函數(shù)式的編程練習(xí)題:給定一個(gè)列表List<value>,比如{1, 4, 9},構(gòu)造一個(gè)List<List<Integer>>,它的成員都是類表{1, 4, 9}的子集——暫時(shí)不考慮元素的順序。{1, 4, 9}的子集是{1, 4, 9}、{1, 4}、{1, 9}、{4, 9}、{1}、{4}、{9}以及{}。

    包括空子集在內(nèi),這樣的子集總共有8個(gè)。每個(gè)子集都使用List表示,這就是答案中期望的List<List>類型。

    對(duì)于“{1, 4, 9}的子集可以劃分為包含1和不包含1的兩部分”也需要特別解釋。不包含1的子集很簡(jiǎn)單就是{4, 9},包含1的子集可以通過(guò)將1插入到{4, 9}的各子集得到。這樣我們就能利用Java,以一種簡(jiǎn)單、自然、自頂向下的函數(shù)式編程方式實(shí)現(xiàn)該程序了(一個(gè)常見(jiàn)的編程錯(cuò)誤是認(rèn)為空的列表沒(méi)有子集)

    static List<List<Integer>> subsets(List<Integer> list) {if (list.isEmpty()) {List<List<Integer>> ans = new ArrayList<>();ans.add(Collections.emptyList());return ans;}Integer first = list.get(0);List<Integer> rest = list.subList(1,list.size());List<List<Integer>> subans = subsets(rest);List<List<Integer>> subans2 = insertAll(first, subans);return concat(subans, subans2); }

    如果給出的輸入是{1, 4, 9},程序最終給出的答案是{{}, {9}, {4}, {4, 9}, {1}, {1, 9}, {1, 4}, {1, 4, 9}}。

    假設(shè)缺失的方法insertAll和concat自身都是函數(shù)式的,并依此推斷你的subsets方法也是函數(shù)式的,因?yàn)樵摲椒ㄖ袥](méi)有任何操作會(huì)修改現(xiàn)有的結(jié)構(gòu)。這就是著名的歸納法。

    static List<List<Integer>> insertAll(Integer first,List<List<Integer>> lists) {List<List<Integer>> result = new ArrayList<>();for (List<Integer> list : lists) {List<Integer> copyList = new ArrayList<>();copyList.add(first);copyList.addAll(list);result.add(copyList);}return result; }

    但是我們希望你不要這樣使用

    static List<List<Integer>> concat(List<List<Integer>> a, List<List<Integer>> b) {a.addAll(b);return a; }

    不過(guò),我們真正建議你采用的是下面這種方式:

    static List<List<Integer>> concat(List<List<Integer>> a, List<List<Integer>> b) {List<List<Integer>> r = new ArrayList<>(a);r.addAll(b);return r; }

    第二個(gè)版本的concat是純粹的函數(shù)式。雖然它在內(nèi)部會(huì)對(duì)對(duì)象進(jìn)行修改(向列表r添加元素),但是它返回的結(jié)果基于參數(shù)卻沒(méi)有修改任何一個(gè)傳入的參數(shù)。與此相反,第一個(gè)版本基于這樣的事實(shí),執(zhí)行完concat(subans, subans2)方法調(diào)用后,沒(méi)人需要再次使用subans的值。對(duì)于我們定義的subsets,這的確是事實(shí),所以使用簡(jiǎn)化版本的concat是個(gè)不錯(cuò)的選擇。不過(guò),這也取決于你如何審視你的時(shí)間,你是愿意為定位詭異的缺陷費(fèi)勁心機(jī)耗費(fèi)時(shí)間呢?還是花費(fèi)些許的代價(jià)創(chuàng)建一個(gè)對(duì)象的副本呢?

    無(wú)論你怎樣解釋這個(gè)不太純粹的concat方法,“只會(huì)用于第一參數(shù)可以被強(qiáng)制覆蓋的場(chǎng)景,或者只會(huì)使用在這個(gè)subsets方法中,任何對(duì)subsets的修改都會(huì)遵照這一標(biāo)準(zhǔn)進(jìn)行代碼評(píng)審”,一旦將來(lái)的某一天,某個(gè)人發(fā)現(xiàn)這段代碼的某些部分可以復(fù)用,并且似乎可以工作時(shí),你未來(lái)調(diào)試的夢(mèng)魘就開始了。

    請(qǐng)牢記:考慮編程問(wèn)題時(shí),采用函數(shù)式的方法,關(guān)注函數(shù)的輸入?yún)?shù)以及輸出結(jié)果(即你希望做什么),通常比設(shè)計(jì)階段的早期就考慮如何做、修改哪些東西要卓有成效得多

    遞歸和迭代

    Recursion

    純粹的函數(shù)式編程語(yǔ)言通常不包含像while或者for這樣的迭代構(gòu)造器。因?yàn)檫@種類型的構(gòu)造器經(jīng)常隱藏著陷阱,誘使你修改對(duì)象。

    比如,while循環(huán)中,循環(huán)的條件需要更新;否則循環(huán)就一次都不會(huì)執(zhí)行,要么就進(jìn)入無(wú)限循環(huán)的狀態(tài)。但是,很多情況下循環(huán)還是非常有用的。

    如果沒(méi)有人能感知的話,函數(shù)式也允許進(jìn)行變更,這意味著我們可以修改局部變量。我們?cè)贘ava中使用的for-each循環(huán),for(Apple a : apples { }如果用迭代器方式重寫,代碼如下:

    Iterator<Apple> it = apples.iterator();while (it.hasNext()) {Apple apple = it.next();// ... }

    這并不是問(wèn)題,因?yàn)楦淖儼l(fā)生時(shí),這些變化(包括使用next方法對(duì)迭代器狀態(tài)的改變以及在while循環(huán)內(nèi)部對(duì)apple變量的賦值)對(duì)于方法的調(diào)用方是不可見(jiàn)的。但是,如果使用for-each循環(huán),比如像下面這個(gè)搜索算法就會(huì)帶來(lái)問(wèn)題,因?yàn)檠h(huán)體會(huì)對(duì)調(diào)用方共享的數(shù)據(jù)結(jié)構(gòu)進(jìn)行修改:

    public void searchForGold(List<String> l, Stats stats){for(String s: l){if("gold".equals(s)){stats.incrementFor("gold");}} }

    實(shí)際上,對(duì)函數(shù)式而言,循環(huán)體帶有一個(gè)無(wú)法避免的副作用:它會(huì)修改stats對(duì)象的狀態(tài),而這和程序的其他部分是共享的。

    由于這個(gè)原因,純函數(shù)式編程語(yǔ)言,比如Haskell直接去除了這樣的帶有副作用的操作!之后你該如何編寫程序呢?比較理論的答案是每個(gè)程序都能使用無(wú)需修改的遞歸重寫,通過(guò)這種方式避免使用迭代。使用遞歸,你可以消除每步都需更新的迭代變量。一個(gè)經(jīng)典的教學(xué)問(wèn)題是用迭代的方式或者遞歸的方式(假設(shè)輸入值大于1)編寫一個(gè)計(jì)算階乘的函數(shù)(參數(shù)為正數(shù)),代碼如下。

    迭代式的階乘計(jì)算

    static int factorialIterative(int n) {int r = 1;for (int i = 1; i <= n; i++) {r *= i;}return r; }

    遞歸式的階乘計(jì)算

    static long factorialRecursive(long n) {return n == 1 ? 1 : n * factorialRecursive(n-1); }

    基于Stream的階乘

    static long factorialStreams(long n){ return LongStream.rangeClosed(1, n).reduce(1, (long a, long b) -> a * b); }

    談?wù)勑蕟?wèn)題。作為Java的用戶,相信你已經(jīng)意識(shí)到函數(shù)式程序的狂熱支持者們總是會(huì)告訴你說(shuō),應(yīng)該使用遞歸,摒棄迭代。然而,通常而言,執(zhí)行一次遞歸式方法調(diào)用的開銷要比迭代執(zhí)行單一機(jī)器級(jí)的分支指令大不少。為什么呢?每次執(zhí)行factorialRecursive方法調(diào)用都會(huì)在調(diào)用棧上創(chuàng)建一個(gè)新的棧幀,用于保存每個(gè)方法調(diào)用的狀態(tài)(即它需要進(jìn)行的乘法運(yùn)算),這個(gè)操作會(huì)一直指導(dǎo)程序運(yùn)行直到結(jié)束。

    這意味著你的遞歸迭代方法會(huì)依據(jù)它接收的輸入成比例地消耗內(nèi)存。這也是為什么如果你使用一個(gè)大型輸入執(zhí)行factorialRecursive方法,很容易遭遇StackOverflowError異常:

    Exception in thread "main" java.lang.StackOverflowError

    這是否意味著遞歸百無(wú)一用呢?當(dāng)然不是!函數(shù)式語(yǔ)言提供了一種方法解決這一問(wèn)題:尾-調(diào)優(yōu)化(tail-call optimization)?;镜乃枷胧悄憧梢跃帉戨A乘的一個(gè)迭代定義,不過(guò)迭代調(diào)用發(fā)生在函數(shù)的最后(所以我們說(shuō)調(diào)用發(fā)生在尾部)。這種新型的迭代調(diào)用經(jīng)過(guò)優(yōu)化后執(zhí)行的速度快很多。作為示例,下面是一個(gè)階乘的“尾-遞”(tail-recursive)定義。

    PS. 迭代與遞歸方法間取長(zhǎng)補(bǔ)短。個(gè)人認(rèn)為接下代碼示例并不好。聯(lián)想到《算法4th》的歸并——插入排序算法,這算法才能更好地為“尾-遞”的示例。

    static long factorialTailRecursive(long n) {return factorialHelper(1, n); }static long factorialHelper(long acc, long n) {return n == 1 ? acc : factorialHelper(acc * n, n-1); }

    方法factorialHelper屬于“尾-遞”類型的函數(shù),原因是遞歸調(diào)用發(fā)生在方法的最后。對(duì)比我們前文中factorialRecursive方法的定義,這個(gè)方法的最后一個(gè)操作是乘以n,從而得到遞歸調(diào)用的結(jié)果。

    這種形式的遞歸是非常有意義的,現(xiàn)在我們不需要在不同的棧幀上保存每次遞歸計(jì)算的中間值,編譯器能夠自行決定復(fù)用某個(gè)棧幀進(jìn)行計(jì)算。實(shí)際上,在factorialHelper的定義中,立即數(shù)(階乘計(jì)算的中間結(jié)果)直接作為參數(shù)傳遞給了該方法。再也不用為每個(gè)遞歸調(diào)用分配單獨(dú)的棧幀用于跟蹤每次遞歸調(diào)用的中間值——通過(guò)方法的參數(shù)能夠直接訪問(wèn)這些值。

    壞消息是,目前Java還不支持這種優(yōu)化。但是使用相對(duì)于傳統(tǒng)的遞歸,“尾-遞”可能是更好的一種方式,因?yàn)樗鼮樽罱K實(shí)現(xiàn)編譯器優(yōu)化開啟了一扇門。很多的現(xiàn)代JVM語(yǔ)言,比如Scala和Groovy都已經(jīng)支持對(duì)這種形式的遞歸的優(yōu)化,最終實(shí)現(xiàn)的效果和迭代不相上下(它們的運(yùn)行速度幾乎是相同的)。這意味著堅(jiān)持純粹函數(shù)式既能享受它的純凈,又不會(huì)損失執(zhí)行的效率。

    使用Java 8進(jìn)行編程時(shí),我們有一個(gè)建議,你應(yīng)該盡量使用Stream取代迭代操作,從而避免變化帶來(lái)的影響。此外,如果遞歸能讓你以更精煉,并且不帶任何副作用的方式實(shí)現(xiàn)算法,你就應(yīng)該用遞歸替換迭代。實(shí)際上,我們看到使用遞歸實(shí)現(xiàn)的例子更加易于閱讀,同時(shí)又易于實(shí)現(xiàn)和理解(比如,我們?cè)谇拔闹姓故镜淖蛹睦?#xff09;,大多數(shù)時(shí)候編程的效率要比細(xì)微的執(zhí)行時(shí)間差異重要得多。

    小結(jié)

    • 從長(zhǎng)遠(yuǎn)看,減少共享的可變數(shù)據(jù)結(jié)構(gòu)能幫助你降低維護(hù)和調(diào)試程序的代價(jià)。
    • 函數(shù)式編程支持無(wú)副作用的方法和聲明式編程。
    • 函數(shù)式方法可以由它的輸入?yún)?shù)及輸出結(jié)果進(jìn)行判斷。
    • 如果一個(gè)函數(shù)使用相同的參數(shù)值調(diào)用,總是返回相同的結(jié)果,那么它是引用透明的。采用遞歸可以取得迭代式的結(jié)構(gòu),比如while循環(huán)。
    • 相對(duì)于Java語(yǔ)言中傳統(tǒng)的遞歸,“尾-遞”可能是一種更好的方式,它開啟了一扇門,讓我們有機(jī)會(huì)最終使用編譯器進(jìn)行優(yōu)化。

    總結(jié)

    以上是生活随笔為你收集整理的《Java8实战》笔记(13):函数式的思考的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。