真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Java8函數(shù)式的思考方法是什么

本篇內(nèi)容介紹了“Java8函數(shù)式的思考方法是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

創(chuàng)新互聯(lián)建站從2013年創(chuàng)立,先為萊陽等服務(wù)建站,萊陽等地企業(yè),進行企業(yè)商務(wù)咨詢服務(wù)。為萊陽企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。

函數(shù)式的思考

實現(xiàn)和維護系統(tǒng)

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

  1. 你會希望它具有良好的結(jié)構(gòu),最好類的結(jié)構(gòu)應(yīng)該反映出系統(tǒng)的結(jié)構(gòu),這樣能便于大家理解;

  2. 甚至軟件工程中還提供了指標,對結(jié)構(gòu)的合理性進行評估,比如耦合性(軟件系統(tǒng)中各組件之間是否相互獨立)以及內(nèi)聚性(系統(tǒng)的各相關(guān)部分之間如何協(xié)作)。

對大多數(shù)程序員而言,最關(guān)心的日常要務(wù)是代碼維護時的調(diào)試:代碼遭遇一些無法預期的值就有可能發(fā)生崩潰。

  • 為什么會發(fā)生這種情況?

  • 它是如何進入到這種狀態(tài)的?

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

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

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

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

  • 那么到底誰對這個列表擁有所屬權(quán)呢?

  • 如果一個類對它進行了修改,會發(fā)生什么情況?

  • 其他的類預期會發(fā)生這種變化嗎?

  • 其他的類又如何得知列表發(fā)生了修改呢?

  • 我們需要通知使用該列表的所有類這一變化嗎?

  • 抑或是不是每個類都應(yīng)該為自己準備一份防御式的數(shù)據(jù)備份以備不時之需呢?

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

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

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

  • 除了構(gòu)造器內(nèi)的初始化操作,對類中數(shù)據(jù)結(jié)構(gòu)的任何修改,包括字段的賦值操作(一個典型的例子是setter方法)。

  • 拋出一個異常。

  • 進行輸入/輸出操作,比如向一個文件寫數(shù)據(jù)。

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

如果構(gòu)成系統(tǒng)的各個組件都能遵守這一原則,該系統(tǒng)就能在完全無鎖的情況下,使用多核的并發(fā)機制,因為任何一個方法都不會對其他的方法造成干擾。

聲明式編程

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

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

How to do

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

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

  • 從列表中取出一個事務(wù),將其與臨時最昂貴事務(wù)進行比較;

  • 如果該事務(wù)開銷更大,就將臨時最昂貴的事務(wù)設(shè)置為該事務(wù);

  • 接著從列表中取出下一個事務(wù),并重復上述操作。

這種“如何做”風格的編程非常適合經(jīng)典的面向?qū)ο缶幊?,有些時候我們也稱之為“命令式”,因為它的特點是它的指令和計算機底層的詞匯非常相近,比如賦值、條件分支以及循環(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 mostExpensive = transactions.stream()
			.max(comparing(Transaction::getValue));

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

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

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

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

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

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

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

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

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

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


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

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

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

函數(shù)式Java編程

編程實戰(zhàn)中,你是無法用Java語言以純粹的函數(shù)式來完成一個程序的。

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

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

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

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

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


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

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

這其中一個典型的例子是除法和開平方運算,如果除法的第二操作數(shù)是0,或者開平方的參數(shù)為負數(shù)就會發(fā)生這樣的情況。以Java那樣拋出一個異常的方式對這些情況進行建模看起來非常自然。這里存在著一定的爭執(zhí),有的作者認為拋出代表嚴重錯誤的異常是可以接受的,但是捕獲異常是一種非函數(shù)式的控制流,因為這種操作違背了我們在黑盒模型中定義的“傳遞參數(shù),返回結(jié)果”的規(guī)則,引出了代表異常處理的第三支箭頭。

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

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

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

引用透明性

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

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

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

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


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

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

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

我們由函數(shù)式編程和(極端)典型的面向?qū)ο缶幊痰膶Ρ热胧诌M行介紹,最終你會發(fā)現(xiàn)Java8認為這些風格其實只是面向?qū)ο蟮囊粋€極端。作為Java程序員,毫無疑問,你一定使用過某種函數(shù)式編程,也一定使用過某些我們稱為極端面向?qū)ο蟮木幊?。由于硬件(比如多核)和程序員期望(比如使用類數(shù)據(jù)庫查詢式的語言去操縱數(shù)據(jù))的變化,促使Java的軟件工程風格在某種程度上愈來愈向函數(shù)式的方向傾斜。

關(guān)于這個問題有兩種觀點。

  1. 一種支持極端的面向?qū)ο螅喝魏问挛锒际菍ο?,程序要么通過更新字段完成操作,要么調(diào)用對與它相關(guān)的對象進行更新的方法。

  2. 另一種觀點支持引用透明的函數(shù)式編程,認為方法不應(yīng)該有(對外部可見的)對象修改。

實際操作中,Java程序員經(jīng)?;煊眠@些風格。你可能會使用包含了可變內(nèi)部狀態(tài)的迭代器遍歷某個數(shù)據(jù)結(jié)構(gòu),同時又通過函數(shù)式的方式計算數(shù)據(jù)結(jié)構(gòu)中的變量之和。

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

SubsetsMain

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

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

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

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

	List> subans = subsets(rest);
	List> 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ù)式的,因為該方法中沒有任何操作會修改現(xiàn)有的結(jié)構(gòu)。這就是著名的歸納法。

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

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

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

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

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

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

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

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

遞歸和迭代

Recursion

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

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

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

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

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

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

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

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

迭代式的階乘計算

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

遞歸式的階乘計算

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

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

Exception in thread "main" java.lang.StackOverflowError

這是否意味著遞歸百無一用呢?當然不是!函數(shù)式語言提供了一種方法解決這一問題:尾-調(diào)優(yōu)化(tail-call optimization)。基本的思想是你可以編寫階乘的一個迭代定義,不過迭代調(diào)用發(fā)生在函數(shù)的最后(所以我們說調(diào)用發(fā)生在尾部)。這種新型的迭代調(diào)用經(jīng)過優(yōu)化后執(zhí)行的速度快很多。作為示例,下面是一個階乘的“尾-遞”(tail-recursive)定義。

PS. 迭代與遞歸方法間取長補短。個人認為接下代碼示例并不好。聯(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ā)生在方法的最后。對比我們前文中factorialRecursive方法的定義,這個方法的最后一個操作是乘以n,從而得到遞歸調(diào)用的結(jié)果。

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

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

使用Java 8進行編程時,我們有一個建議,你應(yīng)該盡量使用Stream取代迭代操作,從而避免變化帶來的影響。此外,如果遞歸能讓你以更精煉,并且不帶任何副作用的方式實現(xiàn)算法,你就應(yīng)該用遞歸替換迭代。實際上,我們看到使用遞歸實現(xiàn)的例子更加易于閱讀,同時又易于實現(xiàn)和理解(比如,我們在前文中展示的子集的例子),大多數(shù)時候編程的效率要比細微的執(zhí)行時間差異重要得多。

“Java8函數(shù)式的思考方法是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!


網(wǎng)站欄目:Java8函數(shù)式的思考方法是什么
文章源于:http://weahome.cn/article/jdpsso.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部