認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用

認識 Java 函數式編程:從 Lambda 表達式到方法引用 |  3 種方法引用

Overview of Content

在這篇文章中,我們將深入探討 Java 函數式編程,全面了解閉包(Closure)與 Callback 的差別,並探討函數式編程與物件導向編程的不同之處;我們將詳細介紹 Java 中 Lambda 表達式的語法與格式,並分析匿名類與 Lambda 表達式之間的差異

此外,本文還將介紹如何自訂函數式介面(FunctionalInterface)以及 Java 內置的函數式介面,幫助您掌握這些強大的工具。在方法引用部分,我們會解釋方法引用、建構函數引用和數組引用的概念,並示範它們的應用場景。

透過這篇文章,你可以獲得學習和應用 Java 函數式編程的有用知識,從而提升你的編程技巧和效率

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


函數式編程

在軟體設計中物件導向範式(面向對象)就是將「數據」進行抽象,使數據擁有個人身份;而軟體設計中的 函數式編程(Functional Programming)則是對 「行為」 進行抽象,這個行為也就是函數,它會將 函數視為參數 來使用(以 C 語言來講可以想成是傳遞函數指標)

閉包 Closure 概念:與 Callback 的差別

Java Lambda 就是閉包(Closure

● 在函數式編程中的其中一個重點就是「閉包, Closure」:

我們將這兩個中文字分開來看… 在數學中,所謂的「包」,只函數與周圍的環境變數綑綁打包;而「閉」則是指這些變數是封閉的,不會與外界有關係,只為該函數服務

運用在函數式編程中,閉包在解決問題時就會使用「不可變的值

ClosureCallback 又有什麼不同

ClosureCallback 是程式設計中經常使用的概念,雖然它們在功能上有些相似,但在語義和應用上有所不同

Closure 的特點在於「環境的綁定」,它可以捕捉並保存所屬環境中的變數,即使在閉包中已經超出了作用域

Callback(回呼)是一個函數,它作為參數傳遞給另一個函數,並在某個時間點調用(通常是在某個操作完成後),並且「不需要捕捉外部變數

回呼函數(Callback)允許非同步操作和事件驅動編程

為何要學函數式編程:與物件導向的差異

為何要學函數式編程,它有什麼好處嗎?

● 函數式編程是學習大數據的基礎

● 經過編譯器優化後,函數式編程通常可以取得更好的效率

編譯階段,Lambda 表達式會被轉換為與目標類型相容的實例;編譯器會產生一個私有的、靜態的、合成的「方法」,並透過 invokedynamic 指令來呼叫該方法

這種實作方式使得 Lambda 表達式的執行更加高效,因為它減少了物件的建立和方法呼叫的開銷

● 可以很好的運用在併發執行,並且這種運行是安全的

因為它在記憶體中使用的是「執行序私有空間 - 棧」,所以可以很好的避免高並發時的同步問題!

● 減少匿名類的使用,減低開銷

類別載入開銷:每次使用匿名內部類別時,都會產生一個新的類別,這增加了類別載入的開銷

物件建立開銷:每次使用匿名內部類別時,都會建立一個新的對象,這增加了物件建立的開銷

與物件導向的差異

物件導向轉為函數編程有思想上的差距,我們可以透過下表來大致理解一下物件導向、函數式編程兩者個差異

範式經典語言核心概念執行機制突破點實作原理目的常見應用
函數式Scheme/Haskell函數計算運算式突破機械思維的限制引入高階函數,將「函數作為資料處理」模擬數學思維,簡化程式、減少副作用微積分計算、數學邏輯
物件式Kotlin/Java物件物件之間的資料交換突破資料、程式碼分離的限制(強化資料的含義)引入封裝、繼承、多型機制迎合人類的認知模式、提高軟體的易用重用性大型複雜「互動式」系統

Java Lambda

Java 的 Lambda 編成是在 JDK 1.8 後引入,如果 Android 開發要使用記得要在 gradle 設定 compileOptions


// Android gradle 中新增

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

匿名類、Lambda 差異

● Java Lambda 是一個「匿名」函數,就是沒有函數名稱,而與其有個很相像的就是「匿名類」,以下以 以 RxJava 的 Subject 類來舉例看看兩者個差異(使用匿名類與 Lambda 都可以達到相同的功能)

A. 匿名類:以下的 Consumer 就是界面,而我們創建的 new Consumer 就是匿名類

image


// 這是匿名類
AsyncSubject.create().subscribe(
    new Consumer<String>() {
        @Override
        public void accept(String s) throws Throwable {
            System.out.println("AsyncSubject: " + s);
        }
    },
    new Consumer<Throwable>() {
        @Override
        public void accept(Throwable s) throws Throwable {
            System.out.println("AsyncSubject Error: " + s.toString());
        }
    },
    new Action() {
        @Override
        public void run() throws Throwable {
            System.out.println("AsyncSubject Complete");
        }
    }
);

B. Lambda 表達: 同樣的目的,但是以下使用 Lambda 的表達比起匿名類更加的精簡(可以用來簡化程式),而它又稱為「匿名函數


// Lambda 表達

AsyncSubject.create().subscribe(
    s -> System.out.println("AsyncSubject: " + s),
    s -> System.out.println("AsyncSubject Error: " + s.toString()),
    () -> System.out.println("AsyncSubject Complete")
);

● 如果沒皆有處過 RxJava 框架的話,這邊舉另外個例子

我們可以以 Java 原生提供的 Thread 類為例,Thread 建構函數會接收一個 Runnable 界面,同樣使用「匿名類」、「匿名函數, Lambda」看看兩者的表面差異

image

A. 使用匿名類表達 Runnable 界面


// Thread's Runnable 匿名類
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");     
    }
});

B. 使用 Lambda 表達:可以看到這樣表達起來更加的簡潔


// Lambda 表達
new Thread(() -> System.out.println("Hello World"));

● 雖說「簡潔」,但是 Lambda 表達式是否合適每個人、團隊呢?這要取決於團隊成員的認可,只要團隊有共同認知即可,不一定用 Lambda 就是最佳的解

Java Lambda 語法、格式

● 在 Java 程式中要使用 Lambda 表達式,需要使用 Java 引入的一個新語法(符號) ->

符號 -> 以左是 Lambda 所需要的參數,以右是要處理的功能 (當 Func 被呼叫時會執行的代碼)


// 左側 -> 右側
// 參數* -> 功能

(參數*) -> {
    // Lambda body
    System.out.println("Runnable lambda");
};

● 每種不同的程式語言在使用 Lambda 表達式時會有不同的表達方式(eg. 像是 swift 就是使用 in 關鍵字)

● 如果 Lambda 表達式 Body 只有一行的話,則可以省略最外層的大括號


(參數*) -> System.out.println("Runnable lambda");

● 以下是一些常用的 Lambda 表達式(匿名函數),我們分為參數與返回值兩個特性對 Lambda 的表達差異來說明

參數數量對 Lambda 表達式的影響

A. 無參數 Lambda 表達式


void noParamLambda() {
    
    Runnable runnable = () -> {
        System.out.println("Runnable lambda");
    };

    // 同上(由於 Lambda body 只有一行,所以可以省略大括號)
    Runnable runnable = () -> System.out.println("Runnable lambda");

}

B. 一個參數的 Lambda 表達式:如果只有一個參數,那也可以省略小括號 ()


void oneParamLambda() {

    Consumer<String> consumer = (msg) -> System.out.println("Consumer lambda: " + msg);

    Consumer<String> consumer2 = msg -> System.out.println("Consumer2 lambda: " + msg);

    Consumer<String> consumer3 = (String msg) -> System.out.println("Consumer lambda: " + msg);
    
}

C. 兩個或以上參數的 Lambda 表達式:不能省略小括號,並且參數放置在小括號內


void twoParamsLambda() {

    IntConvert intConvert = (value1, value2) -> System.out.println("Consumer lambda: " + (value1 + value2));

}

返回值對 Lambda 表達式的影響:Lambda 表達式如同一般的函數,返回值只能有一個

A. 使用 return 關鍵字


void lambdaWithReturn() {

    Comparable<Integer> comparable = value -> {
        int result = 0;
        if( value > 10) {
            result = 1;
        }
        return result;
    };

}

B. 不使用 return 關鍵字:

另外 Lambda 表達式返回值有個特點,如果 Lambda 表達式中有返回值,並且我們也可以將 Lambda 表達式控制在「一行」中表達,那就可以省略 return 關鍵字


void lambdaWithReturnSingleLine() {

    // 由於 Body 只有一行,所以可以省略 return
    Comparable<Integer> comparable = value -> (value > 10 ? 1 : 0);

}

函數式介面

在前面的小節中,我們可以看到匿名類與匿名函數(Lambda)其實差異不大,那我們是否可以在 Java 中建立一個介面,並規定該介面可以轉為匿名函數呢?

答案是可以的~ 在這章中我們就來介紹

image

自訂函數式介面:FunctionalInterface

● 自訂函數式介面需要透過 @FunctionalInterface 這個註釋,該註釋保留到運行期間(Runtime);而使用這個註釋有幾個條件必須遵循,否則會出錯

A. 該註解只能註解在介面(interface)上

image

B. 介面中 只有一個抽象方法 (但可以使用 default、static 定義),稱為函數式介面

image

● 使用 註釋@FunctionalInterface 修飾的範例如下

A. 沒有用註釋修飾,編譯器並不會檢查 (Source),並且擁有一個以上的方法即不是函數式編程,無法取得函數式編程的好處、優點

B. 透過註釋修飾,檢查正確 (一個抽象介面),也可正常轉 Lambda 表達式

C. 雖然通過註釋修飾,但是超過一個以上的方法,無法編譯過


public class TestLambda {

    public static void main(String []args) {

        NonSymbol n = new NonSymbol() {
            @Override
            public void Print(String s) {
                System.out.println("Non Symbol");
            }

            @Override
            public void Print(String s, String s2) {

            }
        };

        HaveSymbol h = s -> System.out.println("Have Symbol");
    }
}

// "1. "        Okay, but not lambda
interface NonSymbol {
    void Print(String s);
    void Print(String s, String s2);
}

// "2. "        Okay
@FunctionalInterface
interface HaveSymbol {
    void Print(String s);
    default void Print(String s, String s2) {
        System.out.println(s + s2);
    }
    static void Print () {
        System.out.println("Static");
    }
}

// "3. "        Error
@FunctionalInterface
interface HaveTwoMethid {
    void Print(String s);
    void Print(String s, String s2);
}

Java 內置函數式介面

● 除了自定義函數式介面之外,Java 也有提供給我們常使用的邏輯判斷介面,說明、範例如下表所示

函數式介面參數返回類Example
Predicate<T>(斷定)<T>boolean鄉民有 30 cm?
Consumer<T>(消費)<T>voidPrint Something
Function<T, R>(功能)<T><R>接收參數 T 加上 10 後返回
Supplier<T>(供應)Non<T>創建工廠,直接返回
UnaryOperator<T>(特殊 Function)<T><T>單個操作數產生的運算結果
BinaryOperator<T>(特殊 Function)<T, T><T>兩數的相加

Predicate 介面 可以用來做簡單的判定

範例如下:鄉民有 30 cm 的判斷


public class TestJavaAPI {

    public static void main(String []args) {
        boolean result = TestPredicate(31, 
                                       // 傳入 Lambda 表達式
                                       integer -> integer >= 30);
        System.out.println("鄉民有 30cm ? " + result);
    }

    private static  <T> boolean TestPredicate(T value, 
                                              Predicate<T> predicate) {
        return predicate.test(value);
    }
}

Consumer 消費者介面:該介面用於消費數據,當呼叫這個介面時就必須傳入相對的數值

範例如下:將傳入的數值,乘以三倍並打印出來


public class TestJavaAPI {

    public static void main(String []args) {
        TestConsumer(30, 
                     // 傳入 Lambda 表達式
                     (t) -> System.out.println("Value * 3 = " + t * 3));
    }

    private static <T extends Integer> void TestConsumer(T value, Consumer<T> consumer) {
        consumer.accept(value);
    }

Function 介面:該介面可以用來接收一個數據,並將其轉換為另外一種數據(可以把它記成一種轉換器)

範例如下:傳入一個字符串返回其長度


public class TestJavaAPI {

    public static void main(String []args) {
        int length = TestFunction("Alien_Pan", s -> s.length());

        System.out.println("Test String Len: " + length);
    }

    public static <T extends String , U extends Integer> U TestFunction (T value, Function<T, U> function) {
        return function.apply(value);
    }
}

Supplier 提供者介面 :該介面可以用來作為「提供者」的角色

範例如下:創建小明個人資料,姓名、身高、體重


class MyInfo {
    String name;
    int height, weight;
    MyInfo(String name, int height, int weight) {
        this.name = name;
        this.height = height;
        this.weight = weight;
    }
}
@RequiresApi(api = Build.VERSION_CODES.N)
public class TestJavaAPI {
    public static void main(String []args) {
        /*  原型
        TestSupplier(new Supplier<MyInfo>() {
            @Override
            public MyInfo get() {
                return null;
            }
        });
         */
        MyInfo info = TestSupplier(() -> new MyInfo("Alien", 170, 65));
        System.out.println("Name: " + info.name);
        System.out.println("Height: " + info.height);
        System.out.println("Weight: " + info.weight);
    }

    public static <T> T TestSupplier(Supplier<T> supplier) {
        return supplier.get();
    }


方法引用

方法引用(Method References)算是 Lambda 表達式函數的一部份

方法引用是一種簡潔且易讀的語法,允許我們「直接引用現有的方法、建構函數、數組」,而 不需要明確地編寫 Lambda 表達式

它在本質上是 Lambda 表達式的一種簡化形式,提供了一種更簡潔的方式來表示某些常見的 Lambda 表達式

方法引用 Method References

● 在 Java 中方法引用時,需要配合 :: 操作符,它將方法名和物件、類的名稱分開… 常見的使用方式如下

A. 物件::建構函數

B. 類::靜態方法

C. 類::實例方法

使用方法引用時我們可以忽略參數的傳入,而這個前提條件則是:必須與方法引用的參數列表 保持一致 才可忽略

● 當使用 Lambda 表達式時,會去實現抽象介面,但如果該介面 已被實現 則可以直接引用,範例如下:


public static void main(String []args) {
    // 1. static 方法
    //Consumer<String> consumer = s -> System.out.println(s);       原型
    Consumer<String> consumer = System.out::println;

    // 2. 與引用方法的參數列表不一致,所以不能使用方法引用
    Consumer<String> consumer_1 = s -> System.out.println("Name: " + s);    

    // 3. 並無修改參數,可使用引用 static
    BinaryOperator<Integer> b = Math::max;

    // 4. 類普通方法的引用
    String s = TestUnaryOperator("Alien", String::toUpperCase);
    System.out.println(s);
}

public static String TestUnaryOperator(String str, UnaryOperator<String> unaryOperator) {
    return unaryOperator.apply(str);
}

建構函數引用 Constructor Reference

● 建構函式(也稱為建構器)也可以使用方法來引用

其引用的格式為 ClassName::new… 只要函數介面與建構器的參數列表匹配,就可以自動與介面中的方法相容(即與介面中的參數列表相吻合);以下是一個範例


class MyInfo {
    String name;
    int height, weight;
    MyInfo() {
    }

    MyInfo(String name, int height, int weight) {
        this.name = name;
        this.height = height;
        this.weight = weight;
    }
}

public static void TestMethod_2() {
    //"1. " 參數列表不吻合,Supplier 是一個無參介面
    Supplier<MyInfo> s1 = () -> new MyInfo("Alien", 100, 35);
    
    //"2. " 使用無參構造器與無參介面吻合,可以使用建構函數引用
    Supplier<MyInfo> s2 = () -> new MyInfo();
    Supplier<MyInfo> s3 = MyInfo::new;
}

A. Supplier<MyInfo> s1 = () -> new MyInfo("Alien", 170, 65); 說明

此處的參數列表不匹配,因為 Supplier 介面是一個無參界面,而建構函數 MyInfo(String name, int height, int weight) 需要三個參數。因此不能使用建構函式引用

B. Supplier<MyInfo> s2 = () -> new MyInfo();Supplier<MyInfo> s3 = MyInfo::new; 說明

這兩個範例中使用了無參構造器,符合 Supplier 介面的無參要求,因此可以使用建構子來參考 MyInfo::new 來取代 Lambda 表達式 () -> new MyInfo()

數組引用 Array References

● 數組引用(Array References)是一種方法引用,允許你使用簡單的語法來創建數組

它使用類似於構造函數引用的語法,即 Type[]::new,並且需要與目標函數接口的參數列表相匹配… 這種方法引用可以使代碼更加簡潔和易讀

範例如下:


public static void TestMethodArray() {
    MyInfo[] m = new MyInfo[3];

    // Java 原型
    Function<Integer, MyInfo[]> function = new Function<Integer, MyInfo[]>() {
        @Override
        public MyInfo[] apply(Integer integer) {
            return new MyInfo[integer];
        }
    };

    // 1. Lambda 原型
    Function<Integer, MyInfo[]> function1 = integer -> new MyInfo[integer];

    // 2. 數組引用
    Function<Integer, MyInfo[]> function2 = MyInfo[]::new;
}

A. Lambda 原型

● 使用 Lambda 表達式來簡化匿名內部類的寫法,這段程式碼與上述匿名內部類的功能相同

integer -> new MyInfo[integer] 是 Lambda 表達式,根據傳入的 integer 參數來創建一個新的 MyInfo 數組

B. 數組引用

● 使用數組引用來進一步簡化 Lambda 表達式,這是一種特殊的構造函數引用語法,用於創建數組

MyInfo[]::new 是數組引用,根據傳入的 integer 參數來創建一個新的 MyInfo 數組


更多的 Java 語言相關文章

Java 語言深入

● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識

深入 Java 物件導向

● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!


Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響