1. 引言
Method reference(中文可譯作「方法參照」)是一種 Java 的特殊語法,允許將既有的方法(或建構函式)作為函式式介面的實作來傳遞。只要簽章一致,就能在需要 lambda 表達式的地方「傳遞」方法。
語法:
Klass::metod
obekt::metod
Klass::new
如果說 lambda 是「隨寫即用」的小函式,那麼 method reference 就是「直接傳遞既有的方法」。這就像與其重抄食譜,不如給對方一個食譜頁面的連結。
直觀範例
與其這樣:
list.forEach(s -> System.out.println(s));
可以這樣:
list.forEach(System.out::println);
看起來很精簡,對吧?
2. method reference 的種類
方法參照有四種主要形式。下面這些就足夠了。
參照靜態方法
語法: Klass::staticheskiyMetod
範例:
Function<Integer, String> intToString = String::valueOf;
System.out.println(intToString.apply(123)); // "123"
使用 lambda 等價寫法:
Function<Integer, String> intToString = i -> String.valueOf(i);
參照物件的非靜態方法
語法: obekt::metod
範例:
PrintStream printer = System.out;
Consumer<String> consumer = printer::println;
consumer.accept("你好,世界!");
等價於 lambda:
Consumer<String> consumer = s -> printer.println(s);
參照類別的非靜態方法
語法: Klass::metod
此處函式式介面的第一個參數會成為呼叫該方法的物件。
範例:
Function<String, Integer> stringLength = String::length;
System.out.println(stringLength.apply("Java")); // 4
此處的 String::length 會被轉換成函式: (String s) -> s.length()
參照建構函式
語法: Klass::new
範例:
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
ArrayList<String> list = listSupplier.get();
等價於 lambda:
Supplier<ArrayList<String>> listSupplier = () -> new ArrayList<>();
3. 何時使用 method reference?
當 lambda 只是呼叫既有方法、沒有額外邏輯時,使用 method reference 能讓程式碼更短、更易讀。
範例:排序清單
與其這樣:
List<String> names = Arrays.asList("伊萬", "彼得", "安娜");
names.sort((a, b) -> a.compareToIgnoreCase(b));
可以這樣:
names.sort(String::compareToIgnoreCase);
範例:處理集合
與其:
list.forEach(s -> System.out.println(s));
可以更精簡:
list.forEach(System.out::println);
範例:元素轉換
與其:
List<String> numbers = Arrays.asList("1", "2", "3");
List<Integer> ints = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
可以改為:
List<Integer> ints = numbers.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
關於 Stream API 與工具類別 Collectors 的更多內容,你會在第 30 級學到 :P
4. method reference 與 lambda 表達式的比較
等價性
method reference 與 lambda 表達式通常可以互換。當簽章一致時,兩者都能實作函式式介面。
範例:
Consumer<String> c1 = s -> System.out.println(s);
Consumer<String> c2 = System.out::println;
什麼時候更適合用 method reference?
- 當 lambda 只是呼叫既有方法,且沒有額外邏輯。
- 為了提高可讀性,尤其在很長的呼叫鏈中。
- 想要明確表達「這裡只是呼叫方法」。
什麼時候不適合用 method reference?
- 需要額外邏輯(驗證、條件、錯誤處理)時。
- 在呼叫方法前需要先轉換參數時。
範例:
list.forEach(s -> {
if (s != null) System.out.println(s);
});
// 這裡不適合用 method reference,只能用 lambda。
5. 實作練習:把 lambda 重寫為 method reference
範例 1:輸出動物名稱
List<String> animals = Arrays.asList("貓", "狗", "鸚鵡");
animals.forEach(animal -> System.out.println(animal));
重寫為:
animals.forEach(System.out::println);
範例 2:把字串轉成數字
List<String> numbers = Arrays.asList("10", "20", "30");
List<Integer> ints = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
重寫為:
List<Integer> ints = numbers.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
範例 3:依名稱排序物件
List<Animal> animalList = ...;
animalList.sort((a, b) -> a.getName().compareTo(b.getName()));
重寫為:
animalList.sort(Comparator.comparing(Animal::getName));
這裡的 Animal::getName 是對類別非靜態方法的參照。
範例 4:透過建構函式建立物件
Supplier<Dog> dogFactory = () -> new Dog();
Dog dog = dogFactory.get();
重寫為:
Supplier<Dog> dogFactory = Dog::new;
Dog dog = dogFactory.get();
6. 簽章匹配如何運作
只有當方法的簽章與函式式介面的抽象方法一致時,才能使用 method reference。
@FunctionalInterface
interface IntToString {
String convert(int value);
}
public class Demo {
public static String intToHex(int value) {
return Integer.toHexString(value);
}
public static void main(String[] args) {
IntToString converter = Demo::intToHex;
System.out.println(converter.convert(255)); // ff
}
}
此處 Demo::intToHex 適用,因為它接受 int 並回傳 String。
7. 方法參照與帶參數的建構函式
即使建構函式帶有參數,只要簽章一致,仍然可以使用 method reference。
@FunctionalInterface
interface AnimalFactory {
Animal create(String name);
}
class Animal {
private String name;
public Animal(String name) { this.name = name; }
public String getName() { return name; }
}
AnimalFactory factory = Animal::new;
Animal cat = factory.create("Barsik");
System.out.println(cat.getName()); // Barsik
8. 使用 method reference 的常見錯誤
錯誤 1:簽章不匹配。
如果方法的簽章與介面的抽象方法不一致,編譯器會報錯。比如介面期望兩個參數,但參照到的卻是只有一個參數的方法。
錯誤 2:嘗試在沒有物件的情況下使用類別的非靜態方法參照。
當使用 Klass::metod 這種形式時,介面的第一個參數會變成呼叫該方法的物件。如果把參數的數量或順序搞錯,就會匹配失敗。
錯誤 3:在需要額外邏輯的地方使用 method reference。
如果需要條件、記錄(logging)或例外處理,請使用 lambda,而不是方法參照。
錯誤 4:參照到多載的方法。
當類別中存在多個同名方法時,編譯器可能無法判斷該選哪一個。有時需要明確標註函式式介面的型別來幫助推斷。
錯誤 5:在沒有物件的情境下使用非靜態方法參照。
例如,String::toUpperCase 在 map 中能正確運作,因為第一個參數就是 String 物件本身。但若在需要靜態方法的情境中使用它,就會導致錯誤。
GO TO FULL VERSION