skip to content
BlogZzz

[Learn] Java Lambda:functional interface 與 method reference

/ 15 min read

Updated:
Table of Contents

第一次看到 Lambda expression,常會覺得 Java 突然省略了太多資訊

(left, right) -> left + right 沒有 class、method 名稱與明確型別,很難直接看出它如何運作

從 anonymous class 開始逐步拆解 Lambda

讀完後應該能理解以下內容

  • Lambda 到底省略了什麼
  • Lambda 如何宣告、保存與執行
  • functional interface 為什麼只能有一個 abstract method
  • Predicate、Function、Consumer 與 Supplier 如何選
  • method reference、變數捕捉與 checked exception 要注意什麼

先用一句話理解 Lambda

Lambda expression 是 functional interface 的精簡實作方式

它讓一小段行為可以先被保存、傳給 method,並在需要時才執行

先不要使用 Stream,從一個最小的加法範例開始

@FunctionalInterface
interface Calculator {
int calculate(int left, int right);
}

Calculator 只規定一件事

接收兩個 int → 回傳一個 int

它沒有決定兩個數字要相加、相減,還是做其他計算

呼叫端只知道要透過 calculate 執行這段行為

從 anonymous class 走到 Lambda

在 Lambda 出現以前,可以使用 anonymous class 實作 Calculator

Calculator add = new Calculator() {
@Override
public int calculate(int left, int right) {
return left + right;
}
};

這段程式真正想表達的是 left + right,其他部分都是建立 interface 實作所需的結構

因為 compiler 已經從左側知道型別是 Calculator,所以可以依序省略重複資訊

先省略 new Calculator()、method 名稱與 access modifier

Calculator add = (int left, int right) -> {
return left + right;
};

compiler 也能從 Calculator.calculate 推斷 parameter type

Calculator add = (left, right) -> {
return left + right;
};

method body 只有一個 expression 時,可以省略大括號與 return

body 內不需要分號,最右側的分號仍然用來結束整個 variable declaration

Calculator add = (left, right) -> left + right;

四種寫法提供的行為相同,Lambda 只是把重複結構拿掉

可以把 Lambda 拆成三個部分閱讀

(left, right) -> left + right
參數 箭頭 body

宣告 Lambda 時只會建立行為,不會立刻執行 body

仍然要呼叫 functional interface 的 method 才會得到結果

Calculator add = (left, right) -> left + right;
int result = add.calculate(10, 5);
System.out.println(result); // 15

Lambda 沒有自己的 method 名稱

這裡呼叫的 calculate 來自 Calculator interface

Lambda 讓行為也能成為參數

一般 parameter 用來傳入資料

static int doubleValue(int value) {
return value * 2;
}

functional interface parameter 則可以傳入一段行為

static int calculate(
int left,
int right,
Calculator operation) {
return operation.calculate(left, right);
}

呼叫時再決定要使用哪一種運算

int sum = calculate(10, 5, (left, right) -> left + right);
int difference = calculate(10, 5, (left, right) -> left - right);
int product = calculate(10, 5, (left, right) -> left * right);
System.out.println(sum); // 15
System.out.println(difference); // 5
System.out.println(product); // 50

calculate method 不需要知道每一種計算細節

它只負責在正確時間把兩個 argument 交給 operation

這就是把行為當成值傳遞的意思

functional interface 是 Lambda 的型別

Lambda 不能脫離型別單獨存在

下面的寫法無法編譯,因為 var 沒有提供足夠資訊讓 compiler 判斷 parameter 與回傳型別

// var add = (left, right) -> left + right;

Lambda 必須出現在 compiler 知道 target type 的位置

// assignment context
Calculator add = (left, right) -> left + right;
// method invocation context
int result = calculate(10, 5, (left, right) -> left + right);
// cast context
var subtract = (Calculator) (left, right) -> left - right;

functional interface 只能有一個 abstract method,這個 method 稱為 functional method 或 Single Abstract Method

@FunctionalInterface
interface TextFormatter {
String format(String text);
}

@FunctionalInterface 不是必要語法,但建議加上

如果不小心加入第二個 abstract method,compiler 會立即報錯

@FunctionalInterface
interface InvalidFormatter {
String format(String text);
// String preview(String text); // 加入後不再是 functional interface
}

default 與 static method 已經有實作,不會被算進唯一的 abstract method

@FunctionalInterface
interface TextFormatter {
String format(String text);
default String formatTwice(String text) {
return format(format(text));
}
static TextFormatter identity() {
return text -> text;
}
}

Lambda 的 parameter 與 return type 必須符合 functional method

TextFormatter upperCase = text -> text.toUpperCase();
// parameter 數量不符
// TextFormatter invalid = (left, right) -> left + right;
// 回傳型別不符
// TextFormatter alsoInvalid = text -> text.length();

同一段 Lambda 語法可以符合不同 interface,所以 target type 是不可缺少的資訊

Calculator add = (left, right) -> left + right;
java.util.function.IntBinaryOperator alsoAdd =
(left, right) -> left + right;

Lambda 語法完整整理

沒有 parameter 時使用空括號

Runnable task = () -> System.out.println("開始執行");

只有一個 parameter,而且沒有明寫型別時,可以省略括號

TextFormatter trim = text -> text.trim();

保留括號也可以

TextFormatter trim = (text) -> text.trim();

有多個 parameter 時必須使用括號

Calculator add = (left, right) -> left + right;

也可以明確寫出所有 parameter type

Calculator add = (int left, int right) -> left + right;

不能只替部分 parameter 寫型別

// Calculator add = (int left, right) -> left + right;

body 只有一個 expression 時,expression 的結果會自動成為回傳值

Calculator add = (left, right) -> left + right;

body 需要多個 statement 時使用大括號

有回傳值的 functional method 必須明確寫 return

Calculator safeDivide = (left, right) -> {
if (right == 0) {
throw new IllegalArgumentException("right 不可為 0");
}
return left / right;
};

沒有回傳值時不需要 return

Runnable task = () -> {
System.out.println("步驟一");
System.out.println("步驟二");
};

建立 Runnable 仍然不會自動執行

Runnable task = () -> System.out.println("開始執行");
task.run();

常用的 functional interface

java.util.function 已經提供多數常見形狀,不需要為每段 Lambda 都建立自己的 interface

InterfaceFunctional method輸入輸出用途
Predicate<T>testTboolean判斷是否符合條件
Function<T, R>applyTR把資料轉成另一種型別
Consumer<T>acceptT無使用資料並產生副作用
Supplier<T>get無T延遲提供資料
UnaryOperator<T>applyTT轉換同一種型別
BinaryOperator<T>applyT、TT合併兩個相同型別
BiFunction<T, U, R>applyT、UR用兩個輸入產生結果

Predicate 回傳 boolean

Predicate<T> 適合驗證、搜尋與篩選條件

import java.util.function.Predicate;
Predicate<String> isLongName = name -> name.length() >= 4;
boolean amyResult = isLongName.test("Amy");
boolean johnResult = isLongName.test("John");
System.out.println(amyResult); // false
System.out.println(johnResult); // true

多個 Predicate 可以使用 and、or 與 negate 組合

Predicate<String> isNotBlank = text -> !text.isBlank();
Predicate<String> hasMinimumLength = text -> text.length() >= 4;
Predicate<String> isValidName =
isNotBlank.and(hasMinimumLength);
System.out.println(isValidName.test("Amy")); // false
System.out.println(isValidName.test("John")); // true

and 與 or 採用 short-circuit evaluation

前面的 Predicate 已經能決定結果時,後面的 Predicate 不會執行

Function 轉換資料

Function<T, R> 接收一個值,轉成另一個值

import java.util.function.Function;
Function<String, Integer> length = text -> text.length();
int result = length.apply("Java");
System.out.println(result); // 4

T 與 R 可以相同,也可以不同

Function<String, Integer>
輸入 輸出

andThen 會先執行左側 Function,再把結果交給右側 Function

Function<String, String> trim = text -> text.trim();
Function<String, String> lowerCase = text -> text.toLowerCase();
Function<String, String> normalize = trim.andThen(lowerCase);
System.out.println(normalize.apply(" JAVA ")); // java

compose 的閱讀方向相反,會先執行 argument,再執行目前的 Function

Function<String, String> alsoNormalize = lowerCase.compose(trim);

這裡的 normalize 與 alsoNormalize 執行順序相同

Consumer 使用資料但不回傳

Consumer<T> 常用於輸出、記錄或把資料送到其他系統

import java.util.function.Consumer;
Consumer<String> print = text -> System.out.println(text);
print.accept("Hello");

Consumer 沒有回傳值,不代表它完全沒有結果

它通常透過輸出、寫檔或修改物件產生 side effect

Supplier 延遲提供資料

Supplier<T> 沒有輸入,在呼叫 get 時才提供結果

import java.util.UUID;
import java.util.function.Supplier;
Supplier<String> createId = () -> UUID.randomUUID().toString();
String firstId = createId.get();
String secondId = createId.get();

Supplier 不保證每次都要建立新物件

它只保證呼叫 get 時會提供符合型別的結果

Operator 保持輸入與輸出型別相同

UnaryOperator<T> 是輸入與輸出相同的 Function

import java.util.function.UnaryOperator;
UnaryOperator<String> normalize =
text -> text.trim().toLowerCase();
String result = normalize.apply(" JAVA ");
System.out.println(result); // java

BinaryOperator<T> 接收兩個相同型別,並回傳相同型別

import java.util.function.BinaryOperator;
BinaryOperator<Integer> add = (left, right) -> left + right;
int result = add.apply(10, 5);
System.out.println(result); // 15

使用 Integer 會發生 boxing 與 unboxing

大量處理 primitive value 時,可以使用 IntPredicate、IntFunction、IntConsumer、IntSupplier 與 IntBinaryOperator 等 specialization

用 Predicate 重寫可變的篩選規則

不使用 Stream 也能看見 Lambda 的用途

先寫一個接收 Predicate<String> 的 method

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
static List<String> select(
List<String> values,
Predicate<String> rule) {
List<String> result = new ArrayList<>();
for (String value : values) {
if (rule.test(value)) {
result.add(value);
}
}
return result;
}

select 負責走訪資料,實際篩選條件則由 rule 決定

同一個 method 可以接收不同 Lambda

List<String> names = List.of("Amy", "Ben", "Cara", "Dan");
List<String> longNames = select(
names,
name -> name.length() >= 4);
List<String> namesStartingWithA = select(
names,
name -> name.startsWith("A"));
System.out.println(longNames); // [Cara]
System.out.println(namesStartingWithA); // [Amy]

這個結構分開了兩種責任

不變的流程:逐一走訪、保存符合條件的資料
可變的行為:什麼條件才算符合

Stream 的 filter 做的事情非常相似,它也接收一個 Predicate

method reference 簡化只呼叫 method 的 Lambda

如果 Lambda body 只是把 parameter 原封不動交給既有 method,可以使用 :: 改寫成 method reference

常見形式有四種

static method

Function<String, Integer> parse = text -> Integer.parseInt(text);
Function<String, Integer> parseReference = Integer::parseInt;

特定物件的 instance method

Consumer<String> print = text -> System.out.println(text);
Consumer<String> printReference = System.out::println;

任意物件的 instance method

Function<String, Integer> length = text -> text.length();
Function<String, Integer> lengthReference = String::length;

String::length 的 String instance 會來自 Function 收到的 parameter

constructor

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
Supplier<List<String>> createList = () -> new ArrayList<>();
Supplier<List<String>> createListReference = ArrayList::new;

method reference 不會立刻呼叫 method

它與 Lambda 一樣,會先建立符合 functional interface 的行為,等 functional method 被呼叫時才執行

如果需要調整 argument、加入判斷或組合多個動作,Lambda 通常會更清楚

Function<String, Integer> trimmedLength =
text -> text.trim().length();

Lambda 如何捕捉外部變數

Lambda 可以讀取 enclosing scope 的 local variable

int passingScore = 60;
Predicate<Integer> hasPassed = score -> score >= passingScore;

被捕捉的 local variable 必須是 final 或 effectively final

effectively final 表示雖然沒有明寫 final,但初始化後沒有再次賦值

int passingScore = 60;
Predicate<Integer> hasPassed = score -> score >= passingScore;
// passingScore = 70; // 再次賦值後無法編譯

可以直接把變數宣告成 final,讓意圖更明確

final int passingScore = 60;
Predicate<Integer> hasPassed = score -> score >= passingScore;

限制的是 local variable 的重新賦值

如果捕捉的是 object reference,object 本身仍然可能被修改

List<String> results = new ArrayList<>();
Consumer<String> save = value -> results.add(value);
save.accept("Java");
System.out.println(results); // [Java]

這段程式可以編譯,但在 Stream 或多執行緒中修改共享狀態會更難推理

能回傳新結果時,通常比依賴外部 mutable state 更安全

Lambda 內的 this 指向 enclosing object,不是另一個匿名物件

final class Greeter {
private final String prefix = "Hello";
Supplier<String> message() {
return () -> this.prefix;
}
}

Lambda 如何處理 checked exception

Lambda 能拋出哪些 checked exception,由 functional method 的 throws declaration 決定

Function.apply 沒有宣告 throws IOException,所以不能直接把 Files.readString 寫成下面這樣

// Function<Path, String> read = path -> Files.readString(path);

可以在 Lambda 內處理,或包裝成適合目前 API 邊界的 unchecked exception

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Function;
Function<Path, String> read = path -> {
try {
return Files.readString(path);
} catch (IOException exception) {
throw new UncheckedIOException("讀取檔案失敗:" + path, exception);
}
};

不要只為了讓 Lambda 變短就吞掉 exception

錯誤仍然要在能合理恢復的層級處理

Lambda 常見錯誤

建立 Lambda 後忘記呼叫

Runnable task = () -> System.out.println("執行工作");

這裡只建立 task,需要呼叫 task.run() 才會執行

block body 忘記 return

// Function<Integer, Integer> doubleValue = value -> {
// int result = value * 2;
// };

functional method 有回傳值時,每一條能正常結束的路徑都必須回傳相容型別

Function<Integer, Integer> doubleValue = value -> {
int result = value * 2;
return result;
};

把每段行為都塞進 Lambda

Lambda 適合短小而且目的單純的行為

內容包含多層判斷、例外處理或大量狀態變更時,可以抽成有名稱的 method

如果 Lambda 只是轉交 argument 給既有 method,還能改成 method reference

Predicate<String> isBlank = value -> value.isBlank();
Predicate<String> isBlankReference = String::isBlank;

有意義的 method 名稱通常比很長的 inline Lambda 更容易測試與閱讀

結論

  • Lambda 是 functional interface 的精簡實作方式
  • Lambda 必須搭配只有一個 abstract method 的 functional interface
  • 宣告 Lambda 只會建立行為,呼叫 functional method 時才會執行 body
  • Lambda 可以當成 argument 傳入 method,讓固定流程接收不同的行為
  • Predicate、Function、Consumer 與 Supplier 涵蓋最常見的行為形狀
  • method reference 適合只轉交既有 method 的簡單 Lambda
  • 被 Lambda 捕捉的 local variable 必須是 final 或 effectively final
  • Lambda 適合短小而單純的行為,複雜邏輯適合抽成有名稱的 method

參考資料