[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,從一個最小的加法範例開始
@FunctionalInterfaceinterface 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); // 15Lambda 沒有自己的 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); // 15System.out.println(difference); // 5System.out.println(product); // 50calculate method 不需要知道每一種計算細節
它只負責在正確時間把兩個 argument 交給 operation
這就是把行為當成值傳遞的意思
functional interface 是 Lambda 的型別
Lambda 不能脫離型別單獨存在
下面的寫法無法編譯,因為 var 沒有提供足夠資訊讓 compiler 判斷 parameter 與回傳型別
// var add = (left, right) -> left + right;Lambda 必須出現在 compiler 知道 target type 的位置
// assignment contextCalculator add = (left, right) -> left + right;
// method invocation contextint result = calculate(10, 5, (left, right) -> left + right);
// cast contextvar subtract = (Calculator) (left, right) -> left - right;functional interface 只能有一個 abstract method,這個 method 稱為 functional method 或 Single Abstract Method
@FunctionalInterfaceinterface TextFormatter { String format(String text);}@FunctionalInterface 不是必要語法,但建議加上
如果不小心加入第二個 abstract method,compiler 會立即報錯
@FunctionalInterfaceinterface InvalidFormatter { String format(String text);
// String preview(String text); // 加入後不再是 functional interface}default 與 static method 已經有實作,不會被算進唯一的 abstract method
@FunctionalInterfaceinterface 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
| Interface | Functional method | 輸入 | 輸出 | 用途 |
|---|---|---|---|---|
Predicate<T> | test | T | boolean | 判斷是否符合條件 |
Function<T, R> | apply | T | R | 把資料轉成另一種型別 |
Consumer<T> | accept | T | 無 | 使用資料並產生副作用 |
Supplier<T> | get | 無 | T | 延遲提供資料 |
UnaryOperator<T> | apply | T | T | 轉換同一種型別 |
BinaryOperator<T> | apply | T、T | T | 合併兩個相同型別 |
BiFunction<T, U, R> | apply | T、U | R | 用兩個輸入產生結果 |
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); // falseSystem.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")); // falseSystem.out.println(isValidName.test("John")); // trueand 與 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); // 4T 與 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 ")); // javacompose 的閱讀方向相反,會先執行 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); // javaBinaryOperator<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
參考資料
- https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.27
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/FunctionalInterface.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/package-summary.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Predicate.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Function.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Consumer.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Supplier.html