關於Java8函數式編程你須要瞭解的幾點

函數式編程與面向對象的設計方法在思路和手段上都各有千秋,在這裏,我將簡要介紹一下函數式編程與面向對象相比的一些特色和差別。java

  1. 函數做爲一等公民

在理解函數做爲一等公民這句話時,讓咱們先來看一下一種很是經常使用的互聯網語言JavaScript,相信你們對它都不會陌生。JavaScript並非嚴格意義上的函數式編程,不過,它也不是屬於嚴格的面向對象。可是,若是你願意,你既能夠把它當作面嚮對象語言,也能夠把它當作函數式語言,所以,稱之爲多範式語言,可能更加合適。編程

若是你使用jQuery,你可能會常用以下的代碼: 數組

$("button").click(function(){  
  $("li").each(function(){  
    alert($(this).text())  
   });  
 });  

注意這裏each()函數的參數,這是一個匿名函數,在遍歷全部的li節點時,會彈出li節點的文本內容。將函數做爲參數傳遞給另一個函數,這是函數式編程的特性之一。安全

 

再來考察另一個案例:多線程

function f1(){  
    var n=1;  
    function f2(){  
      alert(n);  
    }  
    return f2;  
  }  
var result=f1();  
result(); // 1  

 這也是一段JavaScript代碼,在這段代碼中,注意函數f1的返回值,它返回了函數f2。在倒數第2行,返回的f2函數並賦值給result,實際上,此時的result就是一個函數,而且指向f2。對result的調用,就會打印n的值。編程語言

函數能夠做爲另一個函數的返回值,也是函數式編程的重要特色。函數式編程

2.無反作用

函數的反作用指的是函數在調用過程當中,除了給出了返回值外,還修改了函數外部的狀態,好比,函數在調用過程當中,修改了某一個全局狀態。函數式編程認爲,函數的副用做應該被儘可能避免。能夠想象,若是一個函數肆意修改全局或者外部狀態,當系統出現問題時,咱們可能很難判斷到底是哪一個函數引發的問題。這對於程序的調試和跟蹤是沒有好處的。若是函數都是顯式函數,那麼函數的執行顯然不會受到外部或者全局信息的影響,所以,對於調試和排錯是有益的。函數

注意:顯式函數指函數與外界交換數據的惟一渠道就是參數和返回值,顯式函數不會去讀取或者修改函數的外部狀態。與之相對的是隱式函數,隱式函數除了參數和返回值外,還會讀取外部信息,或者可能修改外部信息。性能

然而,徹底的無反作用實際上作不到的。由於系統老是須要獲取或者修改外部信息的。同時,模塊之間的交互也極有多是經過共享變量進行的。若是徹底禁止反作用的出現,也是一件讓人很不愉快的事情。所以,大部分函數式編程語言,如Clojure等,都容許反作用的存在。可是與面向對象相比,這種函數調用的反作用,在函數式編程裏,須要進行有效的限制。優化

申明式的(Declarative)

函數式編程是申明式的編程方式。相對於命令式(imperative)而言,命令式的程序設計喜歡大量使用可變對象和指令。咱們老是習慣於建立對象或者變量,而且修改它們的狀態或者值,或者喜歡提供一系列指令,要求程序執行。這種編程習慣在申明式的函數式編程中有所變化。對於申明式的編程範式,你不在須要提供明確的指令操做,全部的細節指令將會更好的被程序庫所封裝,你要作的只是提出你要的要求,申明你的用意便可。

請看下面一段程序,這一段傳統的命令式編程,爲了打印數組中的值,咱們須要進行一個循環,而且每次須要判斷循環是否結束。在循環體內,咱們要明確地給出須要執行的語句和參數。

 
public static void imperative(){  
         int[]iArr={1,3,4,5,6,9,8,7,4,2};  
         for(int i=0;i<iArr.length;i++){  
                   System.out.println(iArr[i]);  
         }  
}  

與之對應的申明式代碼以下: 

public static void declarative(){  
         int[]iArr={1,3,4,5,6,9,8,7,4,2};  
         Arrays.stream(iArr).forEach(System.out::println);  
}  

能夠看到,變量數組的循環體竟然消失了!println()函數彷佛在這裏也沒有指定任何參數,在此,咱們只是簡單的申明瞭咱們的用意。有關循環以及判斷循環是否結束等操做都被簡單地封裝在程序庫中。

3.尾遞歸優化

遞歸是一種經常使用的編程技巧。使用遞歸一般能夠簡化程序編碼,大幅減小代碼行數。可是遞歸有一個很大的弊病——它老是使用棧空間。可是,程序的棧空間是很是有限的,與堆空間相比,可能相差幾個數量級(棧空間大小一般只有幾百K,而堆空間則一般達到幾百M甚至上百G)。所以,大規模的遞歸操做有可能發生棧空間溢出錯誤,這也限制了遞歸函數的使用,並給系統帶來了必定的風險。

而尾遞歸優化能夠有效地避免這種情況。尾遞歸指遞歸操做處於函數的最後一步。在這種狀況下,該函數的工做其實已經完成(剩餘的工做就是再次調用它本身),此時,只須要簡單得將中間結果傳遞給後繼調用的遞歸函數便可。此時,編譯器就能夠進行一種優化,使當前的函數調用返回,或者用新函數的幀棧覆蓋老函數的幀棧。總之,當遞歸處於函數操做的最後一步時,咱們老是能夠千方百計避免遞歸操做不斷申請棧空間。

大部分函數式編程語言直接或者間接支持尾遞歸優化。

4.不變模式

若是讀者熟悉多線程程序設計,那麼必定對不變模式有全部瞭解。所謂不變,是指對象在建立後,就再也不發生變化。好比,java.lang.String就是不變模式的典型。若是你在Java中建立了一個String實例,不管如何,你都不可能改變整個String的值。好比,當你使用String.replace()函數試圖進行字符串替換時,實際上,原有的字符串對象並不會發生變化,函數自己會返回一個新的String對象,做爲給定字符替換後的返回值。不變的對象在函數式編程中被大量使用。

請看如下代碼:

static int[] arr={1,3,4,5,6,7,8,9,10};  
Arrays.stream(arr).map((x)->x=x+1).forEach(System.out::println);  
System.out.println();  
Arrays.stream(arr).forEach(System.out::println);  
 

代碼第2行看似對每個數組成員執行了加1的操做。可是在操做完成後,在最後一行,打印arr數組全部的成員值時,你仍是會發現,數組成員並無變化!在使用函數式編程時,這種狀態是一種常態,幾乎全部的對象都拒絕被修改。

5.易於並行

因爲對象都處於不變的狀態,所以函數式編程更加易於並行。實際上,你甚至徹底不用擔憂線程安全的問題。咱們之因此要關注線程安全,一個很大的緣由是當多個線程對同一個對象進行寫操做時,容易將這個對象「寫壞」,更專業的說法是「使得對象狀態不一致」。可是,因爲不變模式的存在,對象自建立以來,就不可能發生改變,所以,在多線程環境下,也就沒有必要進行任何同步操做。這樣不只有利於並行化,同時,在並行化後,因爲沒有同步和鎖機制,其性能也會比較好。讀者能夠關注一下java.lang.String對象。很顯然,String對象能夠在多線程中很好的工做,可是,它的每個方法都沒有進行同步處理。

6.更少的代碼

一般狀況下,函數式編程更加簡明扼要,Clojure語言(一種運行於JVM的函數式語言)的愛好者就宣稱,使用Clojure能夠將Java代碼行數減小到原有的十分之一。通常說來,精簡的代碼更易於維護。而Java代碼的冗餘性也是出了名的,大部分對於Java語言的攻擊都會直接針對Java繁瑣,並且死板的語法(但我認爲這也是Java的優勢之一,正如本書第一段提到的「保守的設計思想是Java最大的優點」),然而,引入函數式編程範式後,這種狀況發生了改變。咱們可讓Java用更少的代碼完成更多的工做。

請看下面這個例子,對於數組中每個成員,首先判斷是不是奇數,若是是奇數,則執行加1,並最終打印數組內全部成員。

數組定義:

  1. static int[] arr={1,3,4,5,6,7,8,9,10};  
  2. 傳統的處理方式:  
  3. for(int i=0;i<arr.length;i++){  
  4.          if(arr[i]%2!=0){  
  5.                    arr[i]++;  
  6.          }  
  7.          System.out.println(arr[i]);  
  8. }  

 

使用函數式方式:

Arrays.stream(arr).map(x->(x%2==0?x:x+1)).forEach(System.out::println);

能夠看到,函數式範式更加緊湊並且簡潔。

 感興趣的朋友能夠看看這本電子書《Java8函數式編程入門

相關文章
相關標籤/搜索