---
title: 'Java Lambda 表達式、方法引用'
disqus: kyleAlien
---
Java Lambda 表達式、方法引用
===
## Overview of Content
如有引用參考請詳註出處,感謝 :smile:
> **把行為包裝程物件**,也就是匿名類別,JDK 1.8 後引入
:::success
* 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
本篇文章對應的是 [**認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用**](https://devtechascendancy.com/java-functional-lambda-methods/)
:::
[TOC]
## 函數式編程
在軟體設計中物件導向範式(面向對象)就是將「數據」進行抽象,使數據擁有個人身份;而軟體設計中的 **函數式編程(`Functional Programming`)則是對 「==行為==」 進行抽象**,這個行為也就是函數,它會將 **函數視為參數** 來使用(以 C 語言來講可以想成是傳遞函數指標)
### 閉包 Closure 概念:與 Callback 的差別
> Java Lambda 就是閉包(`Closure`)
* 在函數式編程中的其中一個重點就是「**閉包**, `Closure`」:
我們將這兩個中文字分開來看… 在數學中,所謂的「包」,只函數與周圍的環境變數綑綁打包;而「閉」則是指這些變數是封閉的,不會與外界有關係,只為該函數服務
> 運用在函數式編程中,閉包在解決問題時就會使用「**不可變的值**」
:::warning
* **那 `Closure` 與 `Callback` 又有什麼不同**?
> `Closure` 和 `Callback` 是程式設計中經常使用的概念,雖然它們在功能上有些相似,但在語義和應用上有所不同
* `Closure` 的特點在於「**環境的綁定**」,它可以捕捉並保存所屬環境中的變數,即使在閉包中已經超出了作用域
* `Callback`(回呼)是一個函數,它作為參數傳遞給另一個函數,並在某個時間點調用(通常是在某個操作完成後),並且「**不需要捕捉外部變數**」
> 回呼函數(`Callback`)允許非同步操作和事件驅動編程
:::
### 為何要學函數式編程:與物件導向的差異
* **為何要學函數式編程**,它有什麼好處嗎?
* 函數式編程是學習大數據的基礎
* 經過編譯器優化後,函數式編程通常可以取得更好的效率
編譯階段,Lambda 表達式會被轉換為與目標類型相容的實例;編譯器會產生一個私有的、靜態的、合成的「方法」,並透過 `invokedynamic` 指令來呼叫該方法
這種實作方式使得 Lambda 表達式的執行更加高效,因為它減少了物件的建立和方法呼叫的開銷
* 可以很好的運用在併發執行,並且這種運行是安全的
因為它在記憶體中使用的是「執行序私有空間 - 棧」,所以可以很好的避免高並發時的同步問題!
* 減少匿名類的使用,減低開銷
* **類別載入開銷**:每次使用匿名內部類別時,都會產生一個新的類別,這增加了類別載入的開銷
* **物件建立開銷**:每次使用匿名內部類別時,都會建立一個新的對象,這增加了物件建立的開銷
* **與物件導向的差異**:
物件導向轉為函數編程有思想上的差距,我們可以透過下表來大致理解一下物件導向、函數式編程兩者個差異
| 範式 | 經典語言 | 核心概念 | 執行機制 | 突破點 | 實作原理 | 目的 | 常見應用 |
| - | - | - | - | - | - | - | - |
| 函數式 | Scheme/Haskell | 函數 | 計算運算式 | 突破機械思維的限制 | 引入高階函數,將「函數作為資料處理」 | 模擬數學思維,簡化程式、減少副作用 | 微積分計算、數學邏輯 |
| 物件式 | Kotlin/Java | 物件 | 物件之間的資料交換 | 突破資料、程式碼分離的限制(強化資料的含義) | 引入封裝、繼承、多型機制 | 迎合人類的認知模式、提高軟體的易用重用性 | 大型複雜「互動式」系統 |
## Java Lambda
:::info
* **Java 的 Lambda 編成是在 JDK 1.8 後引入**,如果 Android 開發要使用記得要在 gradle 設定 `compileOptions`
```groovy=
// Android gradle 中新增
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
```
:::
### 匿名類、Lambda 差異
* Java Lambda 是一個「**匿名**」函數,就是沒有函數名稱,而與其有個很相像的就是「**匿名類**」,以下以 以 RxJava 的 Subject 類來舉例看看兩者個差異(使用匿名類與 Lambda 都可以達到相同的功能)
1. **匿名類**:以下的 `Consumer` 就是界面,而我們創建的 `new Consumer` 就是匿名類
> ![image](https://hackmd.io/_uploads/BkIotUBuC.png)
```java=
// 這是匿名類
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");
}
}
);
```
2. **Lambda 表達**: 同樣的目的,但是以下使用 Lambda 的表達比起匿名類更加的精簡(可以用來簡化程式),而它又稱為「**匿名函數**」
```java=
// 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](https://hackmd.io/_uploads/Sy09cISd0.png)
1. **使用匿名類表達 `Runnable` 界面**
```java=
// Thread's Runnable 匿名類
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
});
```
2. **使用 Lambda 表達**:可以看到這樣表達起來更加的簡潔
```java=
// Lambda 表達
new Thread(() -> System.out.println("Hello World"));
```
:::warning
* 雖說「簡潔」,但是 Lambda 表達式是否合適每個人、團隊呢?這要取決於團隊成員的認可,只要團隊有共同認知即可,不一定用 Lambda 就是最佳的解
:::
### Java Lambda 語法、格式
* 在 Java 程式中要使用 Lambda 表達式,需要使用 Java 引入的一個新語法(符號) ==`->`==:
符號 ==`->`== 以左是 Lambda 所需要的參數,以右是要處理的功能 (**當 Func 被呼叫時會執行的代碼**)
```java=
// 左側 -> 右側
// 參數* -> 功能
(參數*) -> {
// Lambda body
System.out.println("Runnable lambda");
};
```
:::info
* 每種不同的程式語言在使用 Lambda 表達式時會有不同的表達方式(eg. 像是 swift 就是使用 `in` 關鍵字)
* 如果 Lambda 表達式 Body 只有一行的話,則可以省略最外層的大括號
```java=
(參數*) -> System.out.println("Runnable lambda");
```
:::
* 以下是一些常用的 Lambda 表達式(匿名函數),我們分為參數與返回值兩個特性對 Lambda 的表達差異來說明
* **參數數量對 Lambda 表達式的影響**
1. **無參數 Lambda 表達式**
```java=
void noParamLambda() {
Runnable runnable = () -> {
System.out.println("Runnable lambda");
};
// 同上(由於 Lambda body 只有一行,所以可以省略大括號)
Runnable runnable = () -> System.out.println("Runnable lambda");
}
```
2. **一個參數的 Lambda 表達式**:如果只有一個參數,那也可以省略小括號 `()`
```java=
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);
}
```
3. **兩個或以上參數的 Lambda 表達式**:不能省略小括號,並且參數放置在小括號內
```java=
void twoParamsLambda() {
IntConvert intConvert = (value1, value2) -> System.out.println("Consumer lambda: " + (value1 + value2));
}
```
* **返回值對 Lambda 表達式的影響**:Lambda 表達式如同一般的函數,返回值只能有一個
1. 使用 `return` 關鍵字
```java=
void lambdaWithReturn() {
Comparable<Integer> comparable = value -> {
int result = 0;
if( value > 10) {
result = 1;
}
return result;
};
}
```
2. 不使用 `return` 關鍵字:
另外 Lambda 表達式返回值有個特點,如果 Lambda 表達式中有返回值,並且我們也可以將 Lambda 表達式控制在「一行」中表達,那就可以省略 `return` 關鍵字
```java=
void lambdaWithReturnSingleLine() {
// 由於 Body 只有一行,所以可以省略 return
Comparable<Integer> comparable = value -> (value > 10 ? 1 : 0);
}
```
## 函數式介面
在前面的小節中,我們可以看到匿名類與匿名函數(Lambda)其實差異不大,那我們是否可以在 Java 中建立一個介面,並規定該介面可以轉為匿名函數呢?
答案是可以的~ 在這章中我們就來介紹
> ![image](https://hackmd.io/_uploads/By2SaQIuC.png)
### 自訂函數式介面:FunctionalInterface
* 自訂函數式介面需要透過 `@FunctionalInterface` 這個註釋,該註釋保留到運行期間(`Runtime`);而使用這個註釋有幾個條件必須遵循,否則會出錯
1. 該註解只能註解在介面(`interface`)上
> ![image](https://hackmd.io/_uploads/H15ORXIuR.png)
2. 介面中 ==只有一個**抽象**方法== (但**可以使用 default、static 定義**),稱為函數式介面
> ![image](https://hackmd.io/_uploads/H1FbkN8_0.png)
* 使用 **註釋**`@FunctionalInterface` 修飾的範例如下
1. 沒有用註釋修飾,編譯器並不會檢查 (Source),並且擁有一個以上的方法即不是函數式編程,無法取得函數式編程的好處、優點
2. 透過註釋修飾,檢查正確 (一個抽象介面),也可正常轉 Lambda 表達式
3. 雖然通過註釋修飾,但是超過一個以上的方法,無法編譯過
```java=
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>` | void | Print Something |
| `Function<T, R>`(功能) | `<T>` | `<R>` | 接收參數 T 加上 10 後返回 |
| `Supplier<T>`(供應) | Non | `<T>` | 創建工廠,直接返回 |
| `UnaryOperator<T>`(特殊 Function) | `<T>` | `<T>` | 單個操作數產生的運算結果 |
| `BinaryOperator<T>`(特殊 Function) | `<T, T>` | `<T>` | 兩數的相加 |
* [**Predicate 介面**](https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html) 可以用來做簡單的判定
範例如下:鄉民有 30 cm 的判斷
```java=
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);
}
}
```
> ![](https://i.imgur.com/AyUKESk.png)
* [**Consumer 消費者介面**](https://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html):該介面用於消費數據,當呼叫這個介面時就必須傳入相對的數值
範例如下:將傳入的數值,乘以三倍並打印出來
```java=
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);
}
```
> ![](https://i.imgur.com/hnesbFI.png)
* [**Function 介面**](https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html):該介面可以用來接收一個數據,並將其轉換為另外一種數據(可以把它記成一種轉換器)
範例如下:傳入一個字符串返回其長度
```java=
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);
}
}
```
> ![](https://i.imgur.com/5hovR4d.png)
* [**Supplier 提供者介面**](https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html) :該介面可以用來作為「提供者」的角色
範例如下:創建小明個人資料,姓名、身高、體重
```java=
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();
}
```
> ![](https://i.imgur.com/9zoJwr7.png)
## 方法引用
方法引用(`Method References`)算是 Lambda 表達式函數的一部份
方法引用是一種簡潔且易讀的語法,允許我們「**直接引用現有的方法、建構函數、數組**」,而 **不需要明確地編寫 Lambda 表達式**
> 它在本質上是 Lambda 表達式的一種簡化形式,提供了一種更簡潔的方式來表示某些常見的 Lambda 表達式
### 方法引用 Method References
* 在 Java 中方法引用時,需要配合 `::` 操作符,它將方法名和物件、類的名稱分開… 常見的使用方式如下
1. 物件`::`建構函數
2. 類`::`靜態方法
3. 類`::`實例方法
:::warning
使用方法引用時我們可以忽略參數的傳入,而這個前提條件則是:**必須與方法引用的參數列表 ==保持一致== 才可忽略**
:::
* 當使用 Lambda 表達式時,會去實現抽象介面,但如果該介面 **已被實現** 則可以直接引用,範例如下:
```java=
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`… 只要函數介面與建構器的參數列表匹配,就可以自動與介面中的方法相容(即與介面中的參數列表相吻合);以下是一個範例
```java=
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;
}
```
1. **`Supplier<MyInfo> s1 = () -> new MyInfo("Alien", 170, 65);` 說明**:
此處的參數列表不匹配,因為 Supplier 介面是一個無參界面,而建構函數 `MyInfo(String name, int height, int weight)` 需要三個參數。因此不能使用建構函式引用
2. **`Supplier<MyInfo> s2 = () -> new MyInfo();` 和 `Supplier<MyInfo> s3 = MyInfo::new;` 說明**:
這兩個範例中使用了無參構造器,符合 Supplier 介面的無參要求,因此可以使用建構子來參考 `MyInfo::new` 來取代 Lambda 表達式 `() -> new MyInfo()`
### 數組引用 Array References
* 數組引用(`Array References`)是一種方法引用,允許你使用簡單的語法來創建數組
它使用類似於構造函數引用的語法,即 `Type[]::new`,並且需要與目標函數接口的參數列表相匹配… 這種方法引用可以使代碼更加簡潔和易讀
範例如下:
```java=
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;
}
```
1. **Lambda 原型**
* 使用 Lambda 表達式來簡化匿名內部類的寫法,這段程式碼與上述匿名內部類的功能相同
* `integer -> new MyInfo[integer]` 是 Lambda 表達式,根據傳入的 integer 參數來創建一個新的 MyInfo 數組
2. **數組引用**
* 使用數組引用來進一步簡化 Lambda 表達式,這是一種特殊的構造函數引用語法,用於創建數組
* `MyInfo[]::new` 是數組引用,根據傳入的 integer 參數來創建一個新的 MyInfo 數組
## Appendix & FAQ
:::info
是簡短了不少的代碼量,不過這要好好熟悉,不然不建議使用,因為**可讀性差**
:::
###### tags: `Java 基礎進階`