摘要:當(dāng)我們希望能界定這二者之間的區(qū)別時(shí),我們將第一種稱為純粹的函數(shù)式編程,后者稱為函數(shù)式編程。函數(shù)式編程我們的準(zhǔn)則是,被稱為函數(shù)式的函數(shù)或方法都只能修改本地變量。另一種觀點(diǎn)支持引用透明的函數(shù)式編程,認(rèn)為方法不應(yīng)該有對(duì)外部可見的對(duì)象修改。
一、實(shí)現(xiàn)和維護(hù)系統(tǒng) 1.共享的可變數(shù)據(jù)
如果一個(gè)方法既不修改它內(nèi)嵌類的狀態(tài),也不修改其他對(duì)象的狀態(tài),使用return返回所有的計(jì)算結(jié)果,那么我們稱其為純粹的或者無副作用的。
副作用就是函數(shù)的效果已經(jīng)超出了函數(shù)自身的范疇。下面是一些例子。
除了構(gòu)造器內(nèi)的初始化操作,對(duì)類中數(shù)據(jù)結(jié)構(gòu)的任何修改,包括字段的賦值操作(一個(gè)典型的例子是setter方法)。
拋出一個(gè)異常。
進(jìn)行輸入/輸出操作,比如向一個(gè)文件寫數(shù)據(jù)。
從另一個(gè)角度來看“無副作用”的話,我們就應(yīng)該考慮不可變對(duì)象。不可變對(duì)象是這樣一種對(duì)象,它們一旦完成初始化就不會(huì)被任何方法修改狀態(tài)。這意味著一旦一個(gè)不可變對(duì)象初始化完畢,它永遠(yuǎn)不會(huì)進(jìn)入到一個(gè)無法預(yù)期的狀態(tài)。你可以放心地共享它,無需保留任何副本,并且由于它們不會(huì)被修改,還是線程安全的。如果構(gòu)成系統(tǒng)的各個(gè)組件都能遵守這一原則,該系統(tǒng)就能在完全無鎖的情況下,使用多核的并發(fā)機(jī)制。
2.聲明式編程如果你希望通過計(jì)算找出列表中最昂貴的事務(wù),摒棄傳統(tǒng)的命令式編程“如何做”的風(fēng)格,采用如下這種“要做什么”風(fēng)格的編程通常被稱為聲明式編程(利用了函數(shù)庫(kù),內(nèi)部迭代)。
Optional3.為什么要采用函數(shù)式編程mostExpensive = transactions.stream() .max(comparing(Transaction::getValue));
使用函數(shù)式編程,你可以實(shí)現(xiàn)更加健壯的程序,還不會(huì)有任何的副作用。
二、什么是函數(shù)式編程對(duì)于“什么是函數(shù)式編程”這一問題最簡(jiǎn)化的回答是“它是一種使用函數(shù)進(jìn)行編程的方式”。
當(dāng)談?wù)摗昂瘮?shù)式”時(shí),我們想說的其實(shí)是“像數(shù)學(xué)函數(shù)那樣——沒有副作用”。由此,編程上的一些精妙問題隨之而來。我們的意思是,每個(gè)函數(shù)都只能使用函數(shù)和像if-then-else這樣的數(shù)學(xué)思想來構(gòu)建嗎?或者,我們也允許函數(shù)內(nèi)部執(zhí)行一些非函數(shù)式的操作,只要這些操作的結(jié)果不會(huì)暴露給系統(tǒng)中的其他部分?換句話說,如果程序有一定的副作用,不過該副作用不會(huì)為其他的調(diào)用者感知,是否我們能假設(shè)這種副作用不存在呢?調(diào)用者不需要知道,或者完全不在意這些副作用,因?yàn)檫@對(duì)它完全沒有影響。當(dāng)我們希望能界定這二者之間的區(qū)別時(shí),我們將第一種稱為純粹的函數(shù)式編程,后者稱為函數(shù)式編程。
1.函數(shù)式 Java 編程我們的準(zhǔn)則是,被稱為“函數(shù)式”的函數(shù)或方法都只能修改本地變量。除此之外,它引用的對(duì)象都應(yīng)該是不可修改的對(duì)象。通過這種規(guī)定,我們期望所有的字段都為final類型,所有的引用類型字段都指向不可變對(duì)象。
要被稱為函數(shù)式,函數(shù)或者方法不應(yīng)該拋出任何異常。
那么,如果不使用異常,你該如何對(duì)除法這樣的函數(shù)進(jìn)行建模呢?答案是請(qǐng)使用Optional類型
最后,作為函數(shù)式的程序,你的函數(shù)或方法調(diào)用的庫(kù)函數(shù)如果有副作用,你必須設(shè)法隱藏它們的非函數(shù)式行為,否則就不能調(diào)用這些方法。
2.引用透明性如果一個(gè)函數(shù)只要傳遞同樣的參數(shù)值,總是返回同樣的結(jié)果,那這個(gè)函數(shù)就是引用透明的。
Java語(yǔ)言中,關(guān)于引用透明性還有一個(gè)比較復(fù)雜的問題。假設(shè)你對(duì)一個(gè)返回列表的方法調(diào)用了兩次。這兩次調(diào)用會(huì)返回內(nèi)存中的兩個(gè)不同列表,不過它們包含了相同的元素。如果這些列表被當(dāng)作可變的對(duì)象值(因此是不相同的),那么該方法就不是引用透明的。如果你計(jì)劃將這些列表作為單純的值(不可修改),那么把這些值看成相同的是合理的,這種情況下該方法是引用透明的。通常情況下,在函數(shù)式編程中,你應(yīng)該選擇使用引用透明的函數(shù)。
3.面向?qū)ο蟮木幊毯秃瘮?shù)式編程的對(duì)比作為Java程序員,毫無疑問,你一定使用過某種函數(shù)式編程,也一定使用過某些我們稱為極端面向?qū)ο蟮木幊獭R环N支持極端的面向?qū)ο螅喝魏问挛锒际菍?duì)象,程序要么通過更新字段完成操作,要么調(diào)用對(duì)與它相關(guān)的對(duì)象進(jìn)行更新的方法。另一種觀點(diǎn)支持引用透明的函數(shù)式編程,認(rèn)為方法不應(yīng)該有(對(duì)外部可見的)對(duì)象修改。
三、遞歸和迭代純粹的函數(shù)式編程語(yǔ)言通常不包含像while或者for這樣的迭代構(gòu)造器。之后你該如何編寫程序呢?比較理論的答案是每個(gè)程序都能使用無需修改的遞歸重寫,通過這種方式避免使用迭代。使用遞歸,你可以消除每步都需更新的迭代變量。
比如階乘
static long factorialStreams(long n){ return LongStream.rangeClosed(1, n) .reduce(1, (long a, long b) -> a * b); }
每次執(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
函數(shù)式語(yǔ)言提供了一種方法解決這一問題:尾調(diào)優(yōu)化(tail-call optimization),基本的思想是你可以編寫階乘的一個(gè)迭代定義,不過迭代調(diào)用發(fā)生在函數(shù)的最后(所以我們說調(diào)用發(fā)生在尾部)。
基于“尾遞”的階乘
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); }
使用棧楨方式的階乘的遞歸定義:
階乘的尾遞定義這里它只使用了一個(gè)棧幀:
壞消息是,目前Java還不支持這種優(yōu)化。很多的現(xiàn)代JVM語(yǔ)言,比如Scala和Groovy都已經(jīng)支持對(duì)這種形式的遞歸的優(yōu)化,最終實(shí)現(xiàn)的效果和迭代不相上下(它們的運(yùn)行速度幾乎是相同的)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/74465.html
摘要:實(shí)戰(zhàn)讀書筆記第一章從方法傳遞到接著上次的,繼續(xù)來了解一下,如果繼續(xù)簡(jiǎn)化代碼。去掉并且生成的數(shù)字是萬,所消耗的時(shí)間循序流并行流至于為什么有時(shí)候并行流效率比循序流還低,這個(gè)以后的文章會(huì)解釋。 《Java8實(shí)戰(zhàn)》-讀書筆記第一章(02) 從方法傳遞到Lambda 接著上次的Predicate,繼續(xù)來了解一下,如果繼續(xù)簡(jiǎn)化代碼。 把方法作為值來傳遞雖然很有用,但是要是有很多類似與isHeavy...
摘要:本文是函數(shù)式編程第二章的讀書筆記。的語(yǔ)法簡(jiǎn)化了使用匿名內(nèi)部類時(shí)的模板代碼,讓程序員專注于編寫想要執(zhí)行的行為,也讓代碼更加簡(jiǎn)潔易讀。中最重要的函數(shù)接口類型推斷為新成員表達(dá)式提供了類型推斷的支持,在不需要聲明參數(shù)類型的表達(dá)式中表現(xiàn)的有為明顯。 本文是「Java 8 函數(shù)式編程」第二章的讀書筆記。 Lambda引入的變化 Lambda表達(dá)式,是一種緊湊的、傳遞行為的方式,從編程思想上來講,...
摘要:正確使用并行流錯(cuò)用并行流而產(chǎn)生錯(cuò)誤的首要原因,就是使用的算法改變了某些共享狀態(tài)。高效使用并行流留意裝箱有些操作本身在并行流上的性能就比順序流差還要考慮流的操作流水線的總計(jì)算成本。 一、并行流 1.將順序流轉(zhuǎn)換為并行流 對(duì)順序流調(diào)用parallel方法: public static long parallelSum(long n) { return Stream.iterate(1L...
摘要:第四章引入流一什么是流流是的新成員,它允許你以聲明性方式處理數(shù)據(jù)集合通過查詢語(yǔ)句來表達(dá),而不是臨時(shí)編寫一個(gè)實(shí)現(xiàn)。 第四章 引入流 一、什么是流 流是Java API的新成員,它允許你以聲明性方式處理數(shù)據(jù)集合(通過查詢語(yǔ)句來表達(dá),而不是臨時(shí)編寫一個(gè)實(shí)現(xiàn))。就現(xiàn)在來說,你可以把它們看成遍歷數(shù)據(jù)集的高級(jí)迭代器。此外,流還可以透明地并行處理,你無需寫任何多線程代碼。 下面兩段代碼都是用來返回低...
摘要:限制編寫并行流,存在一些與非并行流不一樣的約定。底層框架并行流在底層沿用的框架,遞歸式的分解問題,然后每段并行執(zhí)行,最終由合并結(jié)果,返回最后的值。 本書第六章的讀書筆記,也是我這個(gè)系列的最后一篇讀書筆記。后面7、8、9章分別講的測(cè)試、調(diào)試與重構(gòu)、設(shè)計(jì)和架構(gòu)的原則以及使用Lambda表達(dá)式編寫并發(fā)程序,因?yàn)楣P記不好整理,就不寫了,感興趣的同學(xué)自己買書來看吧。 并行化流操作 關(guān)于并行與并發(fā)...
摘要:類或父類中聲明的方法的優(yōu)先級(jí)高于任何聲明為默認(rèn)方法的優(yōu)先級(jí)。只有聲明了一個(gè)默認(rèn)方法。由于比更加具體,所以編譯器會(huì)選擇中聲明的默認(rèn)方法。 如果在現(xiàn)存的接口上引入了非常多的新方法,所有的實(shí)現(xiàn)類都必須進(jìn)行改造,實(shí)現(xiàn)新方法,為了解決這個(gè)問題,Java 8為了解決這一問題引入了一種新的機(jī)制。Java 8中的接口現(xiàn)在支持在聲明方法的同時(shí)提供實(shí)現(xiàn),這聽起來讓人驚訝!通過兩種方式可以完成這種操作。其一...
閱讀 3738·2021-09-09 09:33
閱讀 3024·2019-08-30 15:56
閱讀 3017·2019-08-30 15:56
閱讀 3307·2019-08-30 15:55
閱讀 499·2019-08-30 15:53
閱讀 2179·2019-08-30 15:52
閱讀 662·2019-08-28 18:16
閱讀 2391·2019-08-26 13:51