Java函數語言程式設計:一、函數式介面,lambda表示式和方法參照

2022-10-22 06:02:43

Java函數語言程式設計

什麼是函數語言程式設計

通過整合現有程式碼來產生新的功能,而不是從零開始編寫所有內容,由此我們會得到更加可靠的程式碼,並獲得更高的效率

我們可以這樣理解:物件導向程式設計抽象資料,函數語言程式設計抽象行為。

通常而言,方法會根據所傳遞的資料產生不同的結果,但如果想讓一個方法在每次呼叫時都有不同的表現該怎麼辦?你可能會想到多型,沒錯,多型是一種通過改變實際執行的方法所屬物件,以此來改變實際執行的方法程式碼的方式,許多框架實現IoC本質上也是實現了一種自動化的多型。而這裡我們要聊聊其他的兩種方法。

如果我們直接將程式碼傳遞給方法,就可以控制其行為,Java 8以後,我們可以通過lambda表示式和方法參照這兩個新的方法來實現這一點。


1、函數式介面

什麼是函數式介面

什麼是函數式介面?這是我們理解Lambda表示式以及方法參照的重點,這些介面是lambda表示式和方法參照的目標型別,這裡我們參照一個比較容易理解的說法:函數式介面是一個只有一個抽象方法 (不包含祖先類Object中的公共方法,如hashcode()等) 的介面

當我們在編寫介面時,這種函數式方法模式可以使用@FunctionalInterface註解來強制實施,如果被註解的介面不符合標準那麼就會在編譯時報錯。下面給出例子:

interface Functional{// 是函數式介面
    String speak();
}

interface NoFunctional{// 不是函數式介面
    String speak();
    String laugh();
}

interface IsFunctional{// 是函數式介面,因為toString()是Object祖先類的公共方法,不算在內
    String spaek();
    String toString();
}

它們的意義是什麼呢?

這裡拿出一個例子:

interface Say{
    void say();
}

class Speaker{
    public static void speak(){
        System.out.println("Hello, my friend!");
    }
    
    public static void main(String[] args){
        Say say = Speaker::speak; // 這裡的::表示我們參照了Speaker類的speak方法
        say.say();
        // 輸出Hello, my friend!
    }
}

很神奇是不是?我們直接將一個方法參照賦給了一個介面的物件。這裡初看的話顯然問題一堆,首先方法怎麼能作為物件賦值,其次該類也沒有實現該介面,最後,就算能說通,我們的介面和Speaker類根本沒有相同的方法啊!怎麼就呼叫say.say()效果等同於Speaker.speak()呢?

重要:這是Java 8增加的一個小魔法:如果我們將一個方法參照或一個lambda表示式賦給一個函數式介面(且兩個方法的返回值型別引數型別可以匹配上,方法名並不重要),那麼Java會自動調整這個賦值操作,使其能夠匹配目標介面。
對於底層來說,這裡是Java編譯器建立一個實現了目標介面的類的範例,並將我們的方法參照或lambda表示式包裹在其中。

事實上,很容易預見,在這裡如果我們直接將一個Speaker物件賦給一個Say介面,那麼是無法做到的,因為Speaker雖然符合Say的模式,但卻沒有顯式的實現Say介面。幸運的是,Java 8的函數式介面允許我們直接把一個實體方法賦給這樣的一個介面,這樣語法更好,更簡單。

Java為我們準備了大量的函數式介面,這樣我們就可以儘量避免自己建立大量的介面,這些介面都可以在Java.util.function包中輕鬆找到。
不過Java最多隻準備了具有兩個引數的函數式介面,但是介面又不難寫,只要我們理解了函數式介面的意義和用法,自己寫一個能容納更多引數的函數式介面不過是信手拈來罷了。

// 舉個例子,一個有四個泛型引數的函數式介面
@FunctionalInterface
public interface TriFunction<T, U, V, R>{
    R apply(T t, U u, V v, R r);
}

補充

在Java給出的函數式介面中,我們只能看到一部分涉及基本型別的函數式介面。其餘都是由泛型完成的,為什麼要這麼做呢?估計就是因為對於某些很常用的函數式介面,如整形輸入+Double輸出這樣。如果我們採用泛型Function<Integer, Double>來實現,就涉及到自動裝箱和自動拆箱的效能問題,如果該方法會被大量呼叫,那麼還是直接宣告清楚其中的型別對於我們的整體效能更為有利


2、Lambda表示式

lambda表示式本質上是一個匿名方法,其中以->為分隔符,在其前的是輸入引數,在其後的是返回變數。

lambda表示式是函數式介面的其中一個"成果"——另一個是方法參照,我們可以將相同輸入輸出引數 (型別和數量都匹配) 的lambda表示式賦給一個函數式介面,通過呼叫賦值後的介面再來呼叫我們建立的lambda表示式。

lambda表示式的語法如下所示:

  • 引數
  • ->,它可以讀作產生(produces)
  • 方法體
msg -> msg + "!";

msg -> msg.upperCase();

() -> "hello world";

h -> {
    System.out.println(h + "abcdefg");
    return h.lowerCase();
}

有以下這些問題需要注意:

  • lambda表示式如果只有一個引數,可以只寫該引數不寫括號,但你要知道這是一種特殊情況而不是相反
  • 通常需要括號將引數包裹起來,為了一致性考慮,單個引數時也可以使用括號
  • 沒有引數時,必須要有括號指代輸入的引數
  • 存在多個引數時必須要以括號包裹,逗號分割
  • 如果一句話就可以表示返回值,那麼就直接寫到方法體所在的位置,此時return關鍵字是不合法的
  • 如果一句話無法囊括,那麼就需要使用花括號將所有函數體包裹並以return返回結果

與內部匿名類相比,lambda表示式的可讀性極佳,所以如果你需要使用這樣的方法,你應該掌握lambda表示式。

注意,如果你要利用lambda表示式實現遞迴呼叫自身的話,必須要將該表示式賦值給一個靜態變數或一個範例變數,否則該語句對於編譯器可能過於複雜,會產生編譯錯誤。


3、方法參照

方法參照指向的是方法,通過類名或物件名,跟::,然後跟方法名就可以實現方法參照,注意,這種方法並不需要在方法名後面加上參數列。

className::method; // 注意,沒有括號!

我們可以通過一個介面,參照那些簽名 (方法的引數型別數量以及返回型別) 一致的方法。

需要注意的是,任何方法要被參照,都需要該方法存在其繫結物件,這個物件對於靜態方法而言就是其Class物件,而對於普通方法而言,則需要確儲存在該類物件。

該物件要麼被你建立出來並顯式的賦給方法參照,類似這樣

Object o = new o;
method(o::methodName);

這樣我們的編譯器就知道,這個方法是由我們的物件o來執行的,非常的明確。

但是,還有一種情況就是未繫結的方法參照,即尚未關聯到某個物件的普通方法。對於這種參照,必須先提供物件,才能進行使用。

interface Showable{
    String show(Show1 s);
}

class Show1{
    public String show(){
        return "hello world";
    }
}

public class test {
    public static void main(String[] args){
        Show1 s = new Show1();
        Showable sa = Show1::show;

        System.out.println(s.show());
        // 關鍵就在這裡,我們知道這裡sa實際上沒有範例,show()當然無法呼叫,但實際上這裡最終會呼叫到物件s的show()方法
        // 實際上,這裡Java知道,它必須接受這個引數s,並且在s上面呼叫方法
        // 如果方法有更多引數,只需遵循第一個引數取得是this這種模式
        System.out.println(sa.show(s));
    }
}

若不只一個引數,則如下

public class test {
    public static void main(String[] args){
        Show1 s = new Show1();
        Showable sa = Show1::show;

        System.out.println(s.show(1, 2));
        System.out.println(sa.show(s, 1, 2));
    }

}
interface Showable{
    String show(Show1 s, int a, int b);
}

class Show1{
    public String show(int a, int b){
        return "hello world";
    }
}

實際上,除了普通的方法外,我們還可以通過new代替方法名來參照其構造器方法,這也是非常有用的,由於構造器方法實際上繫結該類,相當於靜態方法,所以我們只需要通過介面直接呼叫即可。