學完本章你將能夠:
說明 Lambda 運算式如何從匿名類別演進而來,並流暢運用各種語法簡寫。
定義自己的函數式介面,並熟練使用 |Function|、|Predicate|、 |Consumer|、|Supplier| 等內建介面。
正確使用四種方法參考,寫出更精簡的程式碼。
理解變數捕獲與 effectively final 規則,避開常見陷阱。
運用 |Comparator| 的函數式組合完成多鍵排序。
用 |Optional| 優雅地處理可能不存在的值,並辨識其反模式。
從本章開始,我們正式邁入資深工程師的領域。你在初級與中級篇已經熟悉物件導向的 思維:把資料與行為封裝進物件。而函數式程式設計提供了另一種互補的視角——把 「行為」本身當成值來傳遞。Java 8 引入的 Lambda 運算式與函數式介面,讓這種 風格得以在強型別的語言中優雅落地,也是後續 Stream API(第 2 章)與 |CompletableFuture|(第 4 章)的語法基礎。
本章的核心觀念只有一個:把程式碼當成資料。過去我們只能把數字、字串、 物件傳給方法;現在我們可以把「一段運算邏輯」也傳進去。掌握這個觀念,你看待 API 設計的角度會徹底改變。
從匿名類別到 Lambda
在 Java 8 之前,若要把「一段行為」傳給方法,唯一的辦法是包成一個物件。以最常見 的 |Runnable|(代表一段無參數、無回傳的工作)為例,我們先看同一個需求的 三種寫法,感受語法如何一步步瘦身。
第一種:具名類別
最原始的做法是為這段行為專門宣告一個類別,實作介面後再建立實體傳入:
/** * 把「要執行的工作」包成一個具名類別。 * 缺點:為了一段簡單邏輯,卻要多寫一個完整類別,非常囉嗦。 */ public class NamedClassDemo
// 實作 Runnable 介面的具名類別 static class PrintTask implements Runnable @Override public void run() // 唯一要實作的抽象方法 System.out.println("以具名類別執行工作");
public static void main(String[] args) Runnable task = new PrintTask(); // 先建立物件 task.run(); // 再呼叫其行為
以具名類別執行工作
第二種:匿名類別
如果這個類別只用一次,替它命名並無意義。匿名類別讓我們「宣告類別」與「建立 物件」一步完成,省去命名:
/** * 匿名類別:就地實作介面並建立物件,不必替類別命名。 * 缺點:new Runnable、@Override、run() 這些樣板仍然佔滿版面。 */ public class AnonymousClassDemo public static void main(String[] args) // 一次完成「實作介面」與「建立物件」 Runnable task = new Runnable() @Override public void run() System.out.println("以匿名類別執行工作"); ; task.run();
以匿名類別執行工作
第三種:Lambda 運算式
觀察上面的匿名類別:|Runnable| 只有一個抽象方法 |run|,所以 「要實作哪個方法」根本不必再說一次。編譯器完全能從變數型別推斷出來。Lambda 就是把這些冗餘全部拿掉,只留下真正重要的參數列與方法主體:
/** * Lambda 運算式:只保留參數列與主體,其餘由編譯器推斷。 * run 沒有參數,所以左側是一對空括號。 */ public class LambdaDemo public static void main(String[] args) // () 對應 run 的空參數列,-> 右側是方法主體 Runnable task = () -> System.out.println("以 Lambda 執行工作"); task.run();
以 Lambda 執行工作
三種寫法產生的效果完全相同,但程式碼從十餘行縮到一行。要特別澄清一個常見誤解: Lambda 不只是匿名類別的語法糖。它們在底層的實作機制不同——匿名類別 會產生獨立的 |.class| 檔,而 Lambda 透過 |invokedynamic| 指令在 執行期動態產生實作,通常更輕量。
Lambda 能取代匿名類別的前提是:該介面只有一個抽象方法。這種介面稱為 「函數式介面」(functional interface)。|Runnable|、|Comparator|、 |Callable| 都符合,所以都能用 Lambda 表達;但若介面有兩個以上的抽象方法, 編譯器就無從推斷你要實作哪一個,只能回頭用匿名類別。
Lambda 語法與函數式介面
Lambda 的語法變形
Lambda 的完整形式是「|(參數列) -> 敘述句 |」,但依情況可層層簡化。 下表整理各種合法的簡寫,請對照記憶:
| 簡寫規則 | 範例 |
|---|---|
| 完整形式 | |(int a, int b) -> return a + b; | |
| 省略參數型別(可推斷) | |(a, b) -> return a + b; | |
| 單一運算式省略大括號與 return | |(a, b) -> a + b| |
| 單一參數可省略括號 | |s -> s.length()| |
| 無參數必須保留空括號 | |() -> 42| |
要注意兩個容易犯錯的地方:其一,只有單一運算式時才能同時省略大括號 與 |return|;一旦寫了大括號,就必須明確 |return|(若有回傳值)。 其二,參數型別要嘛全部省略、要嘛全部寫出,不能只寫一半。
下面兩種寫法都會編譯失敗:|(a, b) -> a + b; |(有大括號卻沒有 |return|,而且 |a + b| 單獨成句不合法),以及 |(int a, b) -> a + b|(型別只寫了一半)。修正方式分別是加上 |return| 與統一省略或補齊型別。
自訂函數式介面與 @FunctionalInterface
任何「只有一個抽象方法」的介面都能作為 Lambda 的目標型別。我們可以自己定義, 並加上 |@FunctionalInterface| 標註。這個標註不是必要的,但強烈建議加上: 它會請編譯器檢查此介面是否確實只有一個抽象方法,避免日後有人不小心 多加方法而破壞了它的函數式特性。
/** * 自訂函數式介面:代表「對兩個整數做某種運算」。 * @FunctionalInterface 讓編譯器強制檢查:有且僅有一個抽象方法。 */ @FunctionalInterface interface Calculator
int apply(int a, int b); // 唯一的抽象方法
// 函數式介面「可以」有預設方法與靜態方法,不影響其函數式本質 default Calculator andThenNegate() // 先做原運算,再把結果取負號;回傳一個新的 Calculator return (a, b) -> -this.apply(a, b);
public class CalculatorDemo
public static void main(String[] args)
// 同一個介面,用不同 Lambda 表達不同運算邏輯 Calculator add = (a, b) -> a + b; Calculator multiply = (a, b) -> a * b;
System.out.println("3 + 5 = " + add.apply(3, 5)); System.out.println("3 * 5 = " + multiply.apply(3, 5)); // 呼叫預設方法,得到「相乘後取負」的新運算 System.out.println("-(3 * 5) = " + multiply.andThenNegate().apply(3, 5));
3 + 5 = 8 3 * 5 = 15 -(3 * 5) = -15
這個範例點出一個關鍵:Lambda 的型別不是「Lambda 型別」,而是它所賦值的那個 函數式介面型別。同樣一段 |(a, b) -> a + b|,賦值給 |Calculator| 時就是 |Calculator|,這稱為「目標型別推斷」。
內建核心函數式介面
實務上我們很少自己定義函數式介面,因為 |java.util.function| 套件已備妥 一整組通用介面。下表是四大家族,務必記熟它們的抽象方法名稱:
| 介面 | 抽象方法 | 語意 | 典型用途 |
|---|---|---|---|
| |Function<T,R>| | |R apply(T)| | 輸入 T 產出 R | 資料轉換 |
| |Predicate<T>| | |boolean test(T)| | 條件判斷 | 過濾 |
| |Consumer<T>| | |void accept(T)| | 消費不回傳 | 印出、寫入 |
| |Supplier<T>| | |T get()| | 無輸入產出 T | 延遲產生值 |
Function:轉換與組合
|Function<T,R>| 把型別 |T| 的輸入轉成型別 |R| 的輸出。它最 強大的地方是可以串接:|andThen| 是「先做自己再做參數」, |compose| 則相反。
import java.util.function.Function;
/** * 示範 Function 的轉換與兩種組合方向。 * andThen(g):先 this 再 g;compose(g):先 g 再 this。 */ public class FunctionDemo
public static void main(String[] args)
Function<Integer, Integer> timesTwo = x -> x * 2; // 乘以 2 Function<Integer, Integer> plusTen = x -> x + 10; // 加 10
// 直接套用 System.out.println("timesTwo(5) = " + timesTwo.apply(5));
// andThen:先乘 2(5->10)再加 10(10->20) Function<Integer, Integer> f1 = timesTwo.andThen(plusTen); System.out.println("先乘2再加10:" + f1.apply(5));
// compose:先加 10(5->15)再乘 2(15->30) Function<Integer, Integer> f2 = timesTwo.compose(plusTen); System.out.println("先加10再乘2:" + f2.apply(5));
timesTwo(5) = 10 先乘2再加10:20 先加10再乘2:30
若運算需要兩個輸入,改用 |BiFunction<T,U,R>|,抽象方法為 |R apply(T, U)|,例如 |(a, b) -> a + b| 可賦值給 |BiFunction<Integer,Integer,Integer>|。
Predicate:條件判斷與邏輯組合
|Predicate<T>| 代表一個回傳布林值的判斷。它的 |and|、|or|、 |negate| 讓我們像堆積木一樣組合複雜條件:
import java.util.function.Predicate;
/** * 用 Predicate 表達條件,並用 and/or/negate 組合。 */ public class PredicateDemo
public static void main(String[] args)
Predicate<Integer> isPositive = n -> n > 0; // 是否為正 Predicate<Integer> isEven = n -> n
// and:兩者皆成立 Predicate<Integer> positiveEven = isPositive.and(isEven); // or:任一成立;negate:反向 Predicate<Integer> positiveOrEven = isPositive.or(isEven); Predicate<Integer> notPositive = isPositive.negate();
System.out.println("6 是正偶數?" + positiveEven.test(6)); System.out.println("-4 是正數或偶數?" + positiveOrEven.test(-4)); System.out.println("-4 非正數?" + notPositive.test(-4));
6 是正偶數?true -4 是正數或偶數?true -4 非正數?true
Consumer 與 Supplier
|Consumer<T>| 接收一個值卻不回傳(例如印出),也支援 |andThen| 依序執行多個動作;|Supplier<T>| 則相反,不接收輸入卻能產出一個值,常用 於「延遲產生」的場景。
import java.util.function.Consumer; import java.util.function.Supplier;
/** * Consumer 消費值、Supplier 供應值。 * Supplier 的「延遲」特性在需要昂貴運算時特別有用。 */ public class ConsumerSupplierDemo
public static void main(String[] args)
// Consumer:接收字串並印出;andThen 串接第二個動作 Consumer<String> print = s -> System.out.println("內容:" + s); Consumer<String> printLen = s -> System.out.println("長度:" + s.length()); print.andThen(printLen).accept("Java");
// Supplier:呼叫 get() 時才真正產生值(此處為目前時間毫秒) Supplier<Long> now = () -> System.currentTimeMillis(); System.out.println("時間戳記非空?" + (now.get() > 0));
內容:Java 長度:4 時間戳記非空?true
UnaryOperator 與基本型別特化
當「輸入與輸出型別相同」時,可用 |UnaryOperator<T>|(單元)與 |BinaryOperator<T>|(二元),它們分別是 |Function<T,T>| 與 |BiFunction<T,T,T>| 的特例,語意更清楚。此外,為避免自動裝箱的效能開銷, 標準庫還提供基本型別特化版本,如 |IntFunction<R>|、 |ToIntFunction<T>|、|IntPredicate|、|IntUnaryOperator| 等。處理大量數值時,優先選用這些版本能省下大量 |Integer| 物件的建立。
如何選介面?先問三個問題:有沒有輸入?有沒有輸出?輸入輸出型別一不一樣? 無輸入有輸出選 |Supplier|;有輸入無輸出選 |Consumer|; 有輸入有輸出且型別不同選 |Function|;型別相同選 |UnaryOperator|; 輸出是布林選 |Predicate|。熟記這張決策表,九成場景都不必自訂介面。
方法參考
當 Lambda 的主體「只是呼叫一個既有方法」時,可以用「方法參考」(method reference)進一步簡化,用雙冒號 |::| 語法。它有四種形式:
| 形式 | 語法 | 等價的 Lambda |
|---|---|---|
| 靜態方法 | |Integer::parseInt| | |s -> Integer.parseInt(s)| |
| 特定物件的實體方法 | |out::println| | |s -> out.println(s)| |
| 任意物件的實體方法 | |String::toUpperCase| | |s -> s.toUpperCase()| |
| 建構子參考 | |ArrayList::new| | |() -> new ArrayList()| |
第三種形式最容易搞混:|String::toUpperCase| 看似沒有接收者,其實是把 「串流中每個元素」當成接收者去呼叫其方法。以下範例一次示範四種形式:
import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier;
/** * 完整示範四種方法參考,並附上等價的 Lambda 對照。 */ public class MethodRefDemo
public static void main(String[] args)
// 1. 靜態方法參考:Integer.parseInt(String) Function<String, Integer> parse = Integer::parseInt; System.out.println("parse(4̈2)̈ = " + parse.apply("42"));
// 2. 特定物件的實體方法:對 System.out 呼叫 println java.io.PrintStream out = System.out; java.util.function.Consumer<String> printer = out::println; printer.accept("特定物件的方法參考");
// 3. 任意物件的實體方法:把輸入字串當接收者呼叫 length Function<String, Integer> length = String::length; System.out.println("length(J̈ava)̈ = " + length.apply("Java"));
// 4. 建構子參考:() -> new ArrayList<>() Supplier<List<String>> maker = ArrayList::new; List<String> list = maker.get(); list.add("由建構子參考產生的清單"); System.out.println(list);
// 補充:兩參數方法可用 BiFunction,如 Integer::sum BiFunction<Integer, Integer, Integer> sum = Integer::sum; System.out.println("sum(3, 4) = " + sum.apply(3, 4));
parse("42") = 42 特定物件的方法參考 length("Java") = 4 [由建構子參考產生的清單] sum(3, 4) = 7
變數捕獲與 effectively final
Lambda 可以使用其外部作用域的區域變數,這稱為「變數捕獲」(variable capture)。但有一條鐵律:被捕獲的區域變數必須是 |final| 或 effectively final(實質上不可變,即宣告後就沒再被指派過)。
import java.util.function.Supplier;
/** * Lambda 捕獲外部區域變數 prefix。 * prefix 宣告後未再被指派,屬於 effectively final,合法。 */ public class CaptureDemo
public static void main(String[] args)
String prefix = "編號-"; // 之後不再更動,實質不可變 int base = 100; // 同上
// Lambda 捕獲 prefix 與 base,記住它們當下的值 Supplier<String> gen = () -> prefix + (base + 1); System.out.println(gen.get());
編號-101
為什麼不能修改被捕獲的區域變數?因為區域變數存放在堆疊上,方法一結束 就消失,但 Lambda 可能在方法返回後才執行(例如交給另一個執行緒)。為避免存取 到已消失的變數,Java 的做法是讓 Lambda 捕獲該變數的值的複本,因此原 變數不能再改變,否則複本與本尊不一致將造成混亂。若你確實需要「可變的計數器」, 正確做法是捕獲一個物件(如陣列 |int[] c = 0;| 或 |AtomicInteger|),改變其內容而非重新指派變數本身。
順帶一提,Lambda 內的 |this| 指向的是外圍類別的實體,而非 Lambda 自己;這與匿名類別不同(匿名類別的 |this| 指向匿名類別實體)。 這也是 Lambda 更貼近「一段程式碼」而非「一個獨立物件」的證據。
Comparator 的函數式組合
排序是函數式風格大放異彩的場景。|Comparator| 提供一系列靜態與預設方法, 讓多鍵排序像說話一樣自然:|comparing| 指定主要排序鍵、 |thenComparing| 追加次要鍵、|reversed| 反轉、|nullsFirst| 處理空值。
import java.util.ArrayList; import java.util.Comparator; import java.util.List;
/** * 用 Comparator 的函數式組合,對員工做多鍵排序: * 先依部門升冪,同部門再依薪資降冪。 */ public class ComparatorDemo
// record 是不可變資料載體,第 8 章會深入介紹 record Employee(String name, String dept, int salary)
public static void main(String[] args)
List<Employee> staff = new ArrayList<>(List.of( new Employee("Alice", "研發", 90000), new Employee("Bob", "研發", 85000), new Employee("Cathy", "行銷", 95000), new Employee("David", "行銷", 95000)));
// 主鍵:部門升冪;次鍵:薪資降冪;末鍵:姓名升冪 Comparator<Employee> order = Comparator.comparing(Employee::dept) .thenComparing( Comparator.comparingInt(Employee::salary) .reversed()) .thenComparing(Employee::name);
staff.sort(order); // 就地排序 staff.forEach(e -> System.out.println( e.dept() + " | " + e.salary() + " | " + e.name()));
行銷 | 95000 | Cathy 行銷 | 95000 | David 研發 | 90000 | Alice 研發 | 85000 | Bob
|thenComparing| 的存在讓「多鍵排序」不再需要手寫層層 |if| 比較。 處理可能為 |null| 的欄位時,用 |Comparator.nullsFirst(...)| 或 |nullsLast(...)| 包住既有比較器,即可安全地把空值排到最前或最後,避免 |NullPointerException|。
Optional:優雅處理不存在的值
Tony Hoare 稱 |null| 的發明是自己「十億美元的錯誤」,因為它讓 |NullPointerException| 成為 Java 最常見的執行期例外。|Optional<T>| 是一個容器,明確表達「這裡可能有值、也可能沒有」,逼迫呼叫者正視空值,而不是 默默地忽略。
建立與取值
建立 |Optional| 有三種方式:|of|(值一定非空,若傳入 |null| 會拋例外)、|ofNullable|(值可能為空)、|empty|(明確的空)。
import java.util.Optional;
/** * 示範 Optional 的建立與 map/filter/orElse。 */ public class OptionalBasics
/** 模擬「依 id 查使用者名稱」,查無資料時回傳空 Optional */ static Optional<String> findName(int id) // ofNullable:把可能為 null 的結果安全包起來 return Optional.ofNullable(id == 1 ? "Alice" : null);
public static void main(String[] args)
// map:有值才轉換(轉大寫);filter:有值且符合條件才保留 String r1 = findName(1) .map(String::toUpperCase) .filter(s -> s.length() > 3) .orElse("(不符合條件)"); System.out.println("id=1:" + r1);
// 查無資料時,整條鏈路都被跳過,最後回傳預設值 String r2 = findName(99) .map(String::toUpperCase) .orElse("(查無此人)"); System.out.println("id=99:" + r2);
id=1:ALICE id=99:(查無此人)
orElse 與 orElseGet 的關鍵差異
兩者都在「值不存在時提供替代」,但差別極其重要:|orElse| 的參數 無論如何都會先被求值,而 |orElseGet| 接受一個 |Supplier|, 只有在真的需要時才呼叫。若替代值的產生有副作用或成本很高,這個差異 會造成明顯後果。
import java.util.Optional;
/** * 對比 orElse 與 orElseGet 的求值時機。 * 即使 Optional 有值,orElse 的參數仍會被求值(副作用照樣發生)。 */ public class OrElseVsOrElseGet
/** 模擬昂貴的預設值運算,順便印出以觀察是否被呼叫 */ static String expensiveDefault() System.out.println(">> 產生預設值(昂貴運算被呼叫了)"); return "預設值";
public static void main(String[] args)
Optional<String> present = Optional.of("實際值");
System.out.println("— 使用 orElse —"); // 即使有值,expensiveDefault() 仍被呼叫(浪費) System.out.println("結果:" + present.orElse(expensiveDefault()));
System.out.println("— 使用 orElseGet —"); // 有值,所以 Supplier 完全不會被呼叫(高效) System.out.println("結果:" + present.orElseGet( OrElseVsOrElseGet::expensiveDefault));
— 使用 orElse — >> 產生預設值(昂貴運算被呼叫了) 結果:實際值 — 使用 orElseGet — 結果:實際值
注意 |orElse| 那組即使 |Optional| 有值,仍印出了「昂貴運算被呼叫 了」。這就是為什麼替代值需要運算時,一律用 |orElseGet|。若值不 存在時應該視為錯誤,則用 |orElseThrow|(可傳入例外的 |Supplier|)。
ifPresent 與反模式
|ifPresent| 在有值時執行動作;Java 9 起的 |ifPresentOrElse| 還能 指定「沒值時」的動作。但 |Optional| 也常被誤用:
|Optional| 的三大反模式,務必避免:
先 |isPresent| 再 |get|:這只是把 |null| 檢查換了寫法,完全沒發揮 |Optional| 的價值。應改用 |map|、|filter|、|orElse| 等鏈式方法。
把 |Optional| 當欄位或方法參數:|Optional| 是 設計來當「方法回傳值」的,用作欄位會增加記憶體與序列化負擔,用作參數 則讓呼叫端更麻煩。參數該用多載或 |@Nullable| 表達。
對集合回傳 |Optional|:空集合本身就代表「沒有」,直接 回傳空的 |List| 即可,不需要 |Optional<List<...>>|。
綜合範例:可組合的資料驗證器
最後我們把本章觀念熔於一爐:用 |Predicate| 的組合能力,打造一個可讀性 極高的驗證器。每條規則都是一個 |Predicate|,用 |and| 串起來就是 完整的驗證邏輯——新增規則時完全不必修改既有程式碼。
import java.util.function.Predicate;
/** * 用 Predicate 組合打造密碼驗證器。 * 每條規則獨立、可重用、可自由組合,體現「開放封閉原則」。 */ public class PasswordValidator
public static void main(String[] args)
// 三條獨立規則 Predicate<String> longEnough = p -> p.length() >= 8; Predicate<String> hasDigit = p -> p.chars().anyMatch(Character::isDigit); Predicate<String> hasUpper = p -> p.chars().anyMatch(Character::isUpperCase);
// 用 and 把三條規則組合成一條完整規則 Predicate<String> valid = longEnough.and(hasDigit).and(hasUpper);
// 逐一驗證測試資料 for (String pwd : new String[]"abc", "abcdefgh", "Abcdef12") System.out.println(pwd + " → " + (valid.test(pwd) ? "通過" : "不通過"));
abc → 不通過 abcdefgh → 不通過 Abcdef12 → 通過
這個設計的威力在於:驗證規則本身成了可傳遞、可組合的值。這正是函數式 程式設計的精髓,也是下一章 Stream API 得以如此強大的根基。
本章小結
Lambda 運算式是「把行為當成值傳遞」的語法,從具名類別、匿名類別一路 演進而來,只保留參數列與主體。
Lambda 的目標型別必須是函數式介面(只有一個抽象方法); 建議加 |@FunctionalInterface| 讓編譯器把關。
|java.util.function| 提供 |Function|、|Predicate|、 |Consumer|、|Supplier| 等通用介面,多數場景不需自訂。
|Function| 用 |andThen|/|compose| 組合, |Predicate| 用 |and|/|or|/|negate| 組合。
方法參考有四種形式(靜態、特定物件、任意物件、建構子),是 Lambda 的 進一步精簡。
被 Lambda 捕獲的區域變數必須是 effectively final,因為捕獲的是值的複本。
|Comparator| 的 |comparing|/|thenComparing|/ |reversed| 讓多鍵排序簡潔清楚。
|Optional| 明確表達「可能沒有值」;|orElse| 一定求值、 |orElseGet| 延遲求值;避免 |isPresent|+|get| 等反模式。
本章練習
觀念題:為什麼只有「單一抽象方法」的介面才能用 Lambda 表達?若一個 介面有兩個抽象方法,編譯器會遇到什麼困難? (提示:想想編譯器如何得知你要實作哪個方法。)
觀念題:|orElse| 與 |orElseGet| 在「值存在」時的行為有何 不同?在什麼情況下這個差異會造成 bug 或效能問題? (提示:關注參數的求值時機與副作用。)
實作題:定義一個函數式介面 |StringTransformer|,其方法接收 |String| 回傳 |String|;用它實作「去頭尾空白後轉大寫」, 並改用內建的 |Function| 加 |andThen| 達成同樣效果。 (提示:|String::trim| 與 |String::toUpperCase|。)
實作題:給定 |List<String>| 姓名清單,用 |Comparator| 先 依字串長度升冪、長度相同再依字典序排序。 (提示:先 |comparingInt(String::length)| 再接 |thenComparing|。)
實作題:寫一個方法 |Optional<Double> divide(int a, int b)|, 當 |b| 為 0 時回傳 |Optional.empty()|,否則回傳商; 呼叫端用 |orElseGet| 在除數為 0 時回傳 |Double.NaN|。 (提示:不要用 |isPresent|+|get|。)
實作題:用 |Predicate| 組合,為一個有姓名與年齡兩個欄位的 |record User| 建立「姓名非空且年齡介於 18 到 65」的驗證器, 並測試三筆邊界資料。 (提示:|Predicate<User>| 各寫一條,再用 |and| 串接。)