函數式編程

函數式編程

函數式接口

官方給出的對於 函數式接口 的概念,可以用一句話來說明:

除了繼承自Object的public方法外,有且只有一個抽象方法的接口,稱之爲 “ 函數式接口 ”

JDK1.7 中的接口

對於這句話,我們首先要理解 jdk1.8 中對於接口的定義。早先在 jdk1.7 的版本中,對於接口的定義(官網已經找不到了,只能從書中或者博客中找到)相對來說比較容易理解:

An interface in Java is similar to a class, but the body of an interface can include only abstract methods and final fields (constants). A class implements an interface by providing code for each method declared by the interface.

jdk1.7 版本中,對於接口,我們只需要把握如下幾個定義:

  • Constants
  • Method signatures
  • Nested types

以下是包含jdk1.7中接口所有屬性的一個示例

public interface Jdk7InterfaceTest {
    int param = 2; //全局變量默認強制是 public static final
    int function(); //抽象方法
    abstract class InnerClass{//抽象內部類,默認強制 public static
        //...
    }
    enum MyEnum{RED,BLUE,GRREN}//枚舉類,默認強制 public static
    interface InnerInteerface{ //嵌套接口,默認強制 public static
        void function();
    }
}

在Java中,接口不是類,而是對類的一組需求描述,類遵循接口描述的統一格式進行定義。所有接口中定義的抽象方法,必須在實現類中提供該方法的實現。Java不支持多繼承,主要是爲了避免多繼承會讓語言本身變得複雜(像C++),效率也會降低。而接口可以提供多繼承的大多數好處,同時避免多重繼承的複雜性和低效性。但如此一來,問題也隨之而來。

如果接口中有個方法的定義是可以確定的(實現該接口的類必須重複實現該方法),如何更優雅的設計這個接口?

這是很早之前有個人提出的問題,對此並沒有很好的解決方式,只是提供了一些折中的方式:通過接口裏的靜態類、藉助工具類的靜態方法等。

JDK1.8 中的接口

JDK1.8 在JDK1.7 的基礎上,增加了一些新的屬性,官方給出的定義:

In the Java programming language, an interface is a reference type, similar to a class, that can contain only constants, method signatures, default methods, static methods, and nested types. Method bodies exist only for default methods and static methods. Interfaces cannot be instantiated—they can only be implemented by classes or extended by other interfaces.


本文來自The Java™ Tutorials ,全文地址請點擊:https://docs.oracle.com/javase/tutorial/java/IandI/createinterface.html

相對於jdk1.7 的接口定義來說,1.8有了許多新的屬性:

  • Default methods
  • Static methods

所以完整的包含 jdk1.8 中的接口所有屬性的接口示例,應該是這樣的:

public interface Jdk8InterfaceTest {
    int param = 2; //全局變量默認強制是 public static final
    int function(); //抽象方法
    abstract class InnerClass{//抽象內部類,默認強制 public static
        //...
    }
    enum MyEnum{RED,BLUE,GRREN}//枚舉類,默認強制 public static
    interface InnerInteerface{ //嵌套接口,默認強制 public static
        void function();
    }

    /**
     * 以下是jdk1.8新增的屬性
     */
    default void method(){ //默認方法
        System.out.println("default method in interface");
    }

    static void staticMethod(){  //靜態方法,默認強制 public
          System.out.print("static method in interface");
    }
}

此時再去討論剛剛的問題——如果接口中有個方法的定義是可以確定的(實現該接口的類必須重複實現該方法),如何更優雅的設計這個接口?已經變的異常簡單。不過這不是本文重點討論的問題,只是藉此說明一下函數式接口中關於接口和抽象方法的概念。jdk8中對於接口定義新增的屬性,只是解決了接口中確定的方法定義問題,而對函數式接口的擴展,纔是本文重點。

那麼,對於函數式接口,我們可以有如下幾層理解:

  • 函數式接口必須包含一個且最多一個抽象方法
  • 這個方法不能是繼承自Object的public方法,但可以包含任意數量的繼承自Object的public方法
  • 函數式接口可以有任意數量的 Constants、Nested types、Default methods、Static methods

一個完整的函數式接口示例:

@FunctionalInterface  //加不加@FunctionalInterface對於接口是不是函數式接口沒有影響,該註解只是提醒編譯器去檢查該接口是否僅包含一個抽象方法
public interface LambdaInterface {

     int add(int x,int y);            //抽象方法,有且只能有一個
     
     int param = 1;                   //全局變量,任意數量
     int param2 = 2;                  //全局變量,任意數量
     String toString();               //繼承自Object的public方法,任意數量
     boolean equals(Object obj);      //繼承自Object的public方法,任意數量
     
     default int del(int x,int y){    //default方法,,任意數量
          return x-y;
     }
     default int mul(int x,int y){    //default方法,,任意數量
          return x*y;
     }
     static void staticMethod(){      //靜態方法,默認強制 public,任意數量
          System.out.print("static method in interface");
     }
     static void staticMethod2(){      //靜態方法,默認強制 public,任意數量
          System.out.print("static method in interface");
     }

}

對比函數式接口與普通接口(普通接口的任意屬性的數量都是任意的,函數式接口僅僅只有抽象方法的數量是固定爲1的),唯一的區別(普通接口也可以定義任意數量的繼承自Object的public方法)在於 抽象方法有且只有一個 ,這一個條件,而這恰恰是 函數式接口 的關鍵。

Java中的Lambda 表達式

Lambda 首先需要有一個函數式接口與之對應,對於上面的函數式接口,我們可以寫出如下的Lamb表達式:

LambdaInterface lambdaInterface = (x,y) -> x + y;
System.out.println(lambdaInterface.add(1,2));  

System.out.println(((LambdaInterface)(a,b)->a+b).add(1,2));

Lambda表達式基本語法

(parameters) -> expression
//或
(parameters) ->{ statements; }

示例:

// 1. 不需要參數,返回值爲 5  
() -> 5  
  
// 2. 接收一個參數,返回其2倍的值。當只有一個參數時,可以省略()括號
x -> 2 * x  
//或
x -> {return 2*x;}

// 3. 接受2個參數,並返回他們的差值  
(x, y) -> x – y  
  
// 4. 接收2個int型整數,所有的參數類型都可以省略
(int x, int y) -> x + y  
  
// 5. 接受一個 string 對象,並在控制檯打印,不返回任何值(看起來像是返回void)  
(String s) -> System.out.print(s)  
    
// 6. 接受一個任何一個對象,轉化爲String類型,並在控制檯打印,右邊有多個語句時,{}不能省略
s -> {String var1 = s.toString();System.out.print(var1);}

這裏主要是理解Lambda表達式語法,以便後面理解函數式編程,對於Lambda的具體應用,參考《Java中Lambda表達式的應用》一文。

類型推斷

Lambda表達式可以省略所有的參數類型,這並不是說Lambda表達式不需要指明類型,而是javac 根據 Lambda 表達式上下文信息就能推斷出參數的正確類型。程序依然要經過類型檢查來保證運行的安全性,但不用再顯式聲明類型罷了。這就是所謂的類型推斷。

類型推斷並不是jdk8所特有的,在jdk1.7中就已經出現了。

Map<String, Integer> oldWordCounts = new HashMap<String, Integer>(); 
Map<String, Integer> diamondWordCounts = new HashMap<>(); 

如上,當我們使用菱形操作符,Java會根據變量類型推斷出真正的類型。

Java8對類型推斷做了改善,比如將構造函數直接傳遞給一個方法,根據方法簽名做推斷:

private void useHashmap(Map<String, String> values);
...
useHashmap(new HashMap<>());//這在java7會出現編譯錯誤

甚至可以根據泛型做推斷

public interface Predicate<T> {
	boolean test(T t);
}
Predicate<Integer> atLeast5 = x -> x > 5;

Predicate 只有一個泛型類型的參數, Integer 用於其中。Lambda表達式實現了 Predicate 接口,因此它的單一參數被推斷爲 Integer 類型。 javac 還可檢查Lambda 表達式的返回值是不是 boolean ,這正是 Predicate 方法的返回類型。

函數式編程理念

對行爲的抽象以及描述

每個人對函數式編程的理解不盡相同。但其核心是:在思考問題時,使用不可變值和函數,函數對一個值進行處理,使之變成另一個值

換言之,函數式編程的重點是函數(行爲)。函數,最直接的理解,就是行爲。面向函數編程的代碼實際上就是“對行爲的描述”。什麼樣的行爲?

@FunctionalInterface
public interface LambdaInterface {
     int add(int x);            //抽象方法,有且只能有一個
}

如上定義了一個函數式接口,我們再用Lambda表達式去調用它。

LambdaInterface lambdaInterface = x -> x*2;//Lambda表達式,-> 是運算符,左邊是輸入,右邊是結果
System.out.println(lambdaInterface.add(1,2));

這裏就是一個 對行爲的描述 —— LambdaInterface 接口中的 add() 就是一個行爲(函數),它說明了結果是入參進行了某種行爲(但具體什麼關係暫未可知)而得到的,之後,利用 Lambda 表達式,將這個行爲具體化,本例中的行爲(函數)用數學方式表示就是 f(x)=x2f(x)=x*2 。代碼 LambdaInterface lambdaInterface = x -> x*2; 就是 對行爲的描述 ,它描述了一種行爲(入參變成結果的變化過程)。

我們再從Lambda 表達式本身去體會這種行爲描述,Lambda 表達式的設計就已經說明了這是一種行爲。

在這裏插入圖片描述

實際上,函數式編程的行爲描述比起數學中的函數更加強大,它不僅是數學領域內的函數,正如面向對象編程中的理念——世間萬物都是對象一樣,世間萬物,只要存在行爲,就可以用函數式編程語句進行描述。最直接的理解就是,在世間萬物都是對象的基礎上,所有的類,只要存在行爲,就可以對此行爲進行描述,包括泛型。而在此基礎上,行爲本身又是一中類型,所以函數式編程允許將行爲作爲參數傳入方法中。比如 new Thread(()->System.out.println("開啓一個新的線程")); 如果說面向對象編程是對數據的抽象,那麼面向函數編程就是對行爲的抽象,二者應該是相輔相成的,目的是爲了更容易編寫出更容易理解的健壯的代碼。

不可變對象

一個不可變對象的狀態在其構造完成之後就不可改變,換句話說,構造函數是唯一一個你可以改變對象的狀態的地方。如果你想要改變一個不可變對象的話,你 不會改變它——而是使用修改後的值來創建一個新的對象,並把你的引用指向它。(String就是構建在Java語言內核中的不可變類的一個典型例子。)不 變性是函數式編程的關鍵,因爲它與儘量減少變化部分的這一目標相一致,這使得對這些部分的推斷更爲容易一些。

不可變對象是函數式編程所推崇的方式。

不可變類驅散了Java中許多典型的令人煩心的事情。

  • 測試的簡便:
    • 測試的存在是爲了檢查代碼中成功發生了的轉變。換句話說,測試的真正目的是驗證改變——改變越多,就需要越多的測試來確保你的做法是正確的。
    • 如果你通過嚴格限制改變來隔離變化發生的地方的話,那麼你就爲錯誤的發生創建了更小的空間,需要測試的地方就更少。因爲變化只會發生構造函數中,因此不變類把編寫單元測試變成了一件微不足道的事情。
    • 你不需要一個拷貝構造函數,你永遠也不需要大汗淋漓地去實現一個clone()方法的那些慘不忍睹的細節。把
  • 用於Map和Set:在被當成鍵來使用時,Java的集合字典中的鍵是不能改變值的,因此,不可變類是非常好用的鍵。
  • 併發處理:不可變對象也是自動線程安全的,不存在同步問題。它們也不可能因爲異常的發生而處於一種未知的或是不期望的狀態中。因爲所有的初始化都發生在構 造階段,這在Java中是一個原子過程,在擁有對象實例之前發生了異常,Joshua Bloch把這稱作失敗的原子性(failure atomicity:):一旦對象已經構造,這種基於不可變性的成功或是失敗就是一錘定音的了。
  • 融合到複合(compositon)抽象
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章