方法引用
一、冗余的Lambda場景
來看一個簡單的函數式接口以應用Lambda表達式:
1 @FunctionalInterface 2 public interface Printable { 3 void print(String str); 4 }
在 Printable 接口當中唯一的抽象方法 print 接收一個字符串參數,目的就是為了打印顯示它。那么通過Lambda來使用它的代碼很簡單:
1 public class Demo01PrintSimple { 2 private static void printString(Printable data) { 3 data.print("Hello, World!"); 4 } 5 public static void main(String[] args) { 6 printString(s ‐> System.out.println(s)); 7 } 8 }
其中 printString 方法只管調用 Printable 接口的 print 方法,而並不管 print 方法的具體實現邏輯會將字符串打印到什么地方去。
而 main 方法通過Lambda表達式指定了函數式接口 Printable 的具體操作方案為:拿到String(類型可推導,所以可省略)數據后,在控制台中輸出它。
二、問題分析
這段代碼的問題在於,對字符串進行控制台打印輸出的操作方案,明明已經有了現成的實現,那就是 System.out對象中的 println(String) 方法。
既然Lambda希望做的事情就是調用 println(String) 方法,那何必自己手動調用呢?
三、用方法引用改進代碼
能否省去Lambda的語法格式(盡管它已經相當簡潔)呢?只要“引用”過去就好了:
1 public class DemoPrintRef { 2 private static void printString(Printable data) { 3 data.print("Hello, World!"); 4 } 5 public static void main(String[] args) { 6 printString(System.out::println); 7 } 8 }
請注意其中的雙冒號 :: 寫法,這被稱為“方法引用”,而雙冒號是一種新的語法。
四、方法引用符
雙冒號 :: 為引用運算符,而它所在的表達式被稱為方法引用。如果Lambda要表達的函數方案已經存在於某個方法的實現中,那么則可以通過雙冒號來引用該方法作為Lambda的替代者。
1、語義分析
例如上例中, System.out 對象中有一個重載的 println(String) 方法恰好就是我們所需要的。那么對於printString 方法的函數式接口參數,對比下面兩種寫法,完全等效:
-
-
- Lambda表達式寫法: s -> System.out.println(s);
- 方法引用寫法: System.out::println
- Lambda表達式寫法: s -> System.out.println(s);
-
第一種語義是指:拿到參數之后經Lambda之手,繼而傳遞給 System.out.println 方法去處理。
第二種等效寫法的語義是指:直接讓 System.out 中的 println 方法來取代Lambda。兩種寫法的執行效果完全樣,而第二種方法引用的寫法復用了已有方案,更加簡潔
注意:Lambda 中 傳遞的參數 一定是方法引用中 的那個方法可以接收的類型,否則會拋出異常
2、推導與省略
如果使用Lambda,那么根據“可推導就是可省略”的原則,無需指定參數類型,也無需指定的重載形式——它們都將被自動推導。而如果使用方法引用,也是同樣可以根據上下文進行推導。
函數式接口是Lambda的基礎,而方法引用是Lambda的孿生兄弟。
下面這段代碼將會調用 println 方法的不同重載形式,將函數式接口改為int類型的參數:
1 @FunctionalInterface 2 public interface PrintableInteger { 3 void print(int str); 4 }
由於上下文變了之后可以自動推導出唯一對應的匹配重載,所以方法引用沒有任何變化:
1 public class DemoPrintOverload { 2 private static void printInteger(PrintableInteger data) { 3 data.print(1024); 4 } 5 public static void main(String[] args) { 6 printInteger(System.out::println); 7 } 8 }
這次方法引用將會自動匹配到 println(int) 的重載形式
五、通過對象名引用成員方法
這是最常見的一種用法,與上例相同。如果一個類中已經存在了一個成員方法:
1 public class MethodRefObject { 2 public void printUpperCase(String str) { 3 System.out.println(str.toUpperCase()); 4 } 5 }
函數式接口仍然定義為:
1 @FunctionalInterface 2 public interface Printable { 3 void print(String str); 4 }
那么當需要使用這個 printUpperCase 成員方法來替代 Printable 接口的Lambda的時候,已經具有了MethodRefObject 類的對象實例,則可以通過對象名引用成員方法,代碼為:
1 public class DemoMethodRef { 2 private static void printString(Printable lambda) { 3 lambda.print("Hello"); 4 } 5 public static void main(String[] args) { 6 MethodRefObject obj = new MethodRefObject(); 7 printString(obj::printUpperCase); 8 } 9 }
六、通過類名稱引用靜態方法
由於在 java.lang.Math 類中已經存在了靜態方法 abs ,所以當我們需要通過Lambda來調用該方法時,有兩種寫法。首先是函數式接口:
1 @FunctionalInterface 2 public interface Calcable { 3 int calc(int num); 4 }
第一種寫法是使用Lambda表達式:
1 public class DemoLambda { 2 private static void method(int num, Calcable lambda) { 3 System.out.println(lambda.calc(num)); 4 } 5 public static void main(String[] args) { 6 method(‐10, n ‐> Math.abs(n)); 7 } 8 }
但是使用方法引用的更好寫法是:
1 public class Demo06MethodRef { 2 private static void method(int num, Calcable lambda) { 3 System.out.println(lambda.calc(num)); 4 } 5 public static void main(String[] args) { 6 method(‐10, Math::abs); 7 } 8 }
在這個例子中,下面兩種寫法是等效的:
-
- Lambda表達式: n -> Math.abs(n)
- 方法引用: Math::abs
七、通過super引用成員方法
如果存在繼承關系,當Lambda中需要出現super調用時,也可以使用方法引用進行替代。首先是函數式接口:
1 @FunctionalInterface 2 public interface Greetable { 3 void greet(); 4 }
然后是父類 Human 的內容:
1 public class Human { 2 public void sayHello() { 3 System.out.println("Hello!"); 4 } 5 }
最后是子類 Man 的內容,其中使用了Lambda的寫法:
1 public class Man extends Human { 2 @Override 3 public void sayHello() { 4 System.out.println("大家好,我是Man!"); 5 } 6 //定義方法method,參數傳遞Greetable接口
7 public void method(Greetable g){ 8 g.greet(); 9 } 10 public void show(){ 11 //調用method方法,使用Lambda表達式
12 method(()‐>{ 13 //創建Human對象,調用sayHello方法
14 new Human().sayHello(); 15 }); 16 //簡化Lambda
17 method(()‐>new Human().sayHello()); 18 //使用super關鍵字代替父類對象
19 method(()‐>super.sayHello()); 20 } 21 }
但是如果使用方法引用來調用父類中的 sayHello 方法會更好,例如另一個子類 Woman :
1 public class Man extends Human { 2 @Override 3 public void sayHello() { 4 System.out.println("大家好,我是Man!"); 5 } 6 //定義方法method,參數傳遞Greetable接口
7 public void method(Greetable g){ 8 g.greet(); 9 } 10 public void show(){ 11 method(super::sayHello); 12 } 13 }
在這個例子中,下面兩種寫法是等效的:
-
- Lambda表達式: () -> super.sayHello()
- 方法引用: super::sayHello
- Lambda表達式: () -> super.sayHello()
八、通過this引用成員方法
this代表當前對象,如果需要引用的方法就是當前類中的成員方法,那么可以使用“this::成員方法”的格式來使用方法引用。首先是簡單的函數式接口:
1 @FunctionalInterface 2 public interface Richable { 3 void buy(); 4 }
下面是一個丈夫 Husband 類:
1 public class Husband { 2 private void marry(Richable lambda) { 3 lambda.buy(); 4 } 5 public void beHappy() { 6 marry(() ‐> System.out.println("買套房子")); 7 } 8 }
開心方法 beHappy 調用了結婚方法 marry ,后者的參數為函數式接口 Richable ,所以需要一個Lambda表達式。
但是如果這個Lambda表達式的內容已經在本類當中存在了,則可以對 Husband 丈夫類進行修改:
1 public class Husband { 2 private void buyHouse() { 3 System.out.println("買套房子"); 4 } 5 private void marry(Richable lambda) { 6 lambda.buy(); 7 } 8 public void beHappy() { 9 marry(() ‐> this.buyHouse()); 10 } 11 }
如果希望取消掉Lambda表達式,用方法引用進行替換,則更好的寫法為:
1 public class Husband { 2 private void buyHouse() { 3 System.out.println("買套房子"); 4 } 5 private void marry(Richable lambda) { 6 lambda.buy(); 7 } 8 public void beHappy() { 9 marry(this::buyHouse); 10 } 11 }
在這個例子中,下面兩種寫法是等效的:
-
- Lambda表達式: () -> this.buyHouse()
- 方法引用: this::buyHouse
- Lambda表達式: () -> this.buyHouse()
九、類的構造器引用
由於構造器的名稱與類名完全一樣,並不固定。所以構造器引用使用 類名稱::new 的格式表示。首先是一個簡單的 Person 類:
1 public class Person { 2 private String name; 3 public Person(String name) { 4 this.name = name; 5 } 6 public String getName() { 7 return name; 8 } 9 public void setName(String name) { 10 this.name = name; 11 } 12 }
然后是用來創建 Person 對象的函數式接口:
1 public interface PersonBuilder { 2 Person buildPerson(String name); 3 }
要使用這個函數式接口,可以通過Lambda表達式:
1 public class DemoLambda { 2 public static void printName(String name, PersonBuilder builder) { 3 System.out.println(builder.buildPerson(name).getName()); 4 } 5 public static void main(String[] args) { 6 printName("趙麗穎", name ‐> new Person(name)); 7 } 8 }
但是通過構造器引用,有更好的寫法:
1 public class Demo10ConstructorRef { 2 public static void printName(String name, PersonBuilder builder) { 3 System.out.println(builder.buildPerson(name).getName()); 4 } 5 public static void main(String[] args) { 6 printName("趙麗穎", Person::new); 7 } 8 }
在這個例子中,下面兩種寫法是等效的:
-
- Lambda表達式: name -> new Person(name)
- 方法引用: Person::new
十、數組的構造器引用
數組也是 Object 的子類對象,所以同樣具有構造器,只是語法稍有不同。如果對應到Lambda的使用場景中時,需要一個函數式接口:
1 @FunctionalInterface 2 public interface ArrayBuilder { 3 int[] buildArray(int length); 4 }
在應用該接口的時候,可以通過Lambda表達式:
1 public class DemoArrayInitRef { 2 private static int[] initArray(int length, ArrayBuilder builder) { 3 return builder.buildArray(length); 4 } 5 public static void main(String[] args) { 6 int[] array = initArray(10, length ‐> new int[length]); 7 } 8 }
但是更好的寫法是使用數組的構造器引用 :
1 public class DemoArrayInitRef { 2 private static int[] initArray(int length, ArrayBuilder builder) { 3 return builder.buildArray(length); 4 } 5 public static void main(String[] args) { 6 int[] array = initArray(10, int[]::new); 7 } 8 }
在這個例子中,下面兩種寫法是等效的:
-
- Lambda表達式: length -> new int[length]
- 方法引用: int[]::new