《java 8 實戰》讀書筆記 -第十三章 函數式的思考

1、實現和維護系統

1.共享的可變數據

若是一個方法既不修改它內嵌類的狀態,也不修改其餘對象的狀態,使用return返回全部的計算結果,那麼咱們稱其爲純粹的或者無反作用的
反作用就是函數的效果已經超出了函數自身的範疇。下面是一些例子。java

  • 除了構造器內的初始化操做,對類中數據結構的任何修改,包括字段的賦值操做(一個典型的例子是setter方法)。
  • 拋出一個異常。
  • 進行輸入/輸出操做,好比向一個文件寫數據。

從另外一個角度來看「無反作用」的話,咱們就應該考慮不可變對象。不可變對象是這樣一種對象,它們一旦完成初始化就不會被任何方法修改狀態。這意味着一旦一個不可變對象初始化完畢,它永遠不會進入到一個沒法預期的狀態。你能夠放心地共享它,無需保留任何副本,而且因爲它們不會被修改,仍是線程安全的。若是構成系統的各個組件都能遵照這一原則,該系統就能在徹底無鎖的狀況下,使用多核的併發機制程序員

2.聲明式編程

若是你但願經過計算找出列表中最昂貴的事務,摒棄傳統的命令式編程「如何作」的風格,採用以下這種「要作什麼」風格的編程一般被稱爲聲明式編程(利用了函數庫,內部迭代)。編程

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

3.爲何要採用函數式編程

使用函數式編程,你能夠實現更加健壯的程序,還不會有任何的反作用。安全

2、什麼是函數式編程

對於「什麼是函數式編程」這一問題最簡化的回答是「它是一種使用函數進行編程的方式」。數據結構

當談論「函數式」時,咱們想說的實際上是「像數學函數那樣——沒有反作用」。由此,編程上的一些精妙問題隨之而來。咱們的意思是,每一個函數都只能使用函數和像if-then-else這樣的數學思想來構建嗎?或者,咱們也容許函數內部執行一些非函數式的操做,只要這些操做的結果不會暴露給系統中的其餘部分?換句話說,若是程序有必定的反作用,不過該反作用不會爲其餘的調用者感知,是否咱們能假設這種反作用不存在呢?調用者不須要知道,或者徹底不在乎這些反作用,由於這對它徹底沒有影響。當咱們但願能界定這兩者之間的區別時,咱們將第一種稱爲純粹的函數式編程,後者稱爲函數式編程併發

1.函數式 Java 編程

咱們的準則是,被稱爲「函數式」的函數或方法都只能修改本地變量。除此以外,它引用的對象都應該是不可修改的對象。經過這種規定,咱們指望全部的字段都爲final類型,全部的引用類型字段都指向不可變對象。編程語言

要被稱爲函數式,函數或者方法不該該拋出任何異常。
那麼,若是不使用異常,你該如何對除法這樣的函數進行建模呢?答案是請使用 Optional<T>類型

最後,做爲函數式的程序,你的函數或方法調用的庫函數若是有反作用,你必須設法隱藏它們的非函數式行爲,不然就不能調用這些方法。函數式編程

2.引用透明性

若是一個函數只要傳遞一樣的參數值,老是返回一樣的結果,那這個函數就是引用透明的。函數

Java語言中,關於引用透明性還有一個比較複雜的問題。假設你對一個返回列表的方法調用了兩次。這兩次調用會返回內存中的兩個不一樣列表,不過它們包含了相同的元素。若是這些列表被看成可變的對象值(所以是不相同的),那麼該方法就不是引用透明的。若是你計劃將這些列表做爲單純的值(不可修改),那麼把這些值當作相同的是合理的,這種狀況下該方法是引用透明的。一般狀況下,在函數式編程中,你應該選擇使用引用透明的函數優化

3.面向對象的編程和函數式編程的對比

做爲Java程序員,毫無疑問,你必定使用過某種函數式編程,也必定使用過某些咱們稱爲極端面向對象的編程。一種支持極端的面向對象:任何事物都是對象,程序要麼經過更新字段完成操做,要麼調用對與它相關的對象進行更新的方法。另外一種觀點支持引用透明的函數式編程,認爲方法不該該有(對外部可見的)對象修改。

3、遞歸和迭代

純粹的函數式編程語言一般不包含像while或者for這樣的迭代構造器。以後你該如何編寫程序呢?比較理論的答案是每一個程序都能使用無需修改的遞歸重寫,經過這種方式避免使用迭代。使用遞歸,你能夠消除每步都需更新的迭代變量。
好比階乘

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

每次執行factorialRecursive方法調用都會在調用棧上建立一個新的棧幀,用於保存每一個方法調用的狀態(即它須要進行的乘
法運算),這個操做會一直指導程序運行直到結束。這意味着你的遞歸迭代方法會依據它接收的輸入成比例地消耗內存。這也是爲何若是你使用一個大型輸入執行factorialRecursive方法,很容易遭遇StackOverflowError異常:

Exception in thread "main" java.lang.StackOverflowError

函數式語言提供了一種方法解決這一問題:尾調優化(tail-call optimization),基本的思想是你能夠編寫階乘的一個迭代定義,不過迭代調用發生在函數的最後(因此咱們說調用發生在尾部)。
基於「尾遞」的階乘

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); 
}

使用棧楨方式的階乘的遞歸定義:
圖片描述

階乘的尾遞定義這裏它只使用了一個棧幀:
圖片描述

壞消息是,目前Java還不支持這種優化。不少的現代JVM語言,好比Scala和Groovy都已經支持對這種形式的遞歸的優化,最終實現的效果和迭代不相上下(它們的運行速度幾乎是相同的)。