方法引用
在使用Lambda表達式的時候,我們實際上傳遞進去的代碼就是一種解決方案:拿什么參數做什么操作。那么考慮一種情況:如果我們在Lambda中所指定的操作方案,已經有地方存在相同方案,那是否還有必要再寫重復邏輯?
冗余的Lambda場景
來看一個簡單的函數式接口以應用Lambda表達式:
@FunctionalInterface
public interface Printable {
/**
* 接收一個字符串參數,打印顯示它
* @param str 字符串
*/
public abstract void print(String str);
}
在 Printable 接口當中唯一的抽象方法 print 接收一個字符串參數,目的就是為了打印顯示它。那么通過Lambda來使用它的代碼很簡單:
public class Demo01PrintSimple {
public static void main(String[] args) {
printString(s -> System.out.println(s));
}
private static void printString(Printable data) {
data.print("Hello, World!");
}
}
其中 printString 方法只管調用 Printable 接口的 print 方法,而並不管 print 方法的具體實現邏輯會將字符串打印到什么地方去。而 main 方法通過Lambda表達式指定了函數式接口 Printable 的具體操作方案為:拿到 String(類型可推導,所以可省略)數據后,在控制台中輸出它。
問題分析
這段代碼的問題在於,對字符串進行控制台打印輸出的操作方案,明明已經有了現成的實現,那就是 System.out 對象中的println(String) 方法。既然Lambda希望做的事情就是調用 println(String) 方法,那何必自己手動調用呢?
用方法引用改進代碼
public class Demo02PrintRef {
public static void main(String[] args) {
printString(System.out::println);
}
private static void printString(Printable data) {
data.print("Hello, World!");
}
}
請注意其中的雙冒號 :: 寫法,這被稱為“方法引用”,而雙冒號是一種新的語法。
方法引用符
雙冒號 :: 為引用運算符,而它所在的表達式被稱為方法引用。如果Lambda要表達的函數方案已經存在於某個方法的實現中,那么則可以通過雙冒號來引用該方法作為Lambda的替代者。
語義分析
例如上例中, System.out 對象中有一個重載的 println(String) 方法恰好就是我們所需要的。那么對於 printString 方法的函數式接口參數,對比下面兩種寫法,完全等效:
// Lambda表達式寫法
s -> System.out.println(s);
// 方法引用寫法
System.out::println
- 第一種語義是指:拿到參數之后經Lambda之手,繼而傳遞給 System.out.println 方法去處理。
- 第二種等效寫法的語義是指:直接讓 System.out 中的 println 方法來取代Lambda。兩種寫法的執行效果完全一 樣,而第二種方法引用的寫法復用了已有方案,更加簡潔。
注:Lambda 中 傳遞的參數 一定是方法引用中 的那個方法可以接收的類型,否則會出現編譯錯誤(是錯誤,不是異常)
如上例中:
(int s) -> System.out.println(s)
錯誤如下:
Error:(x, y) java: 不兼容的類型: lambda 表達式中的參數類型不兼容
x、y為定位錯誤的位置,可以理解成直角坐標系中的點坐標。
推導與省略
如果使用Lambda,那么根據“可推導就是可省略”的原則,無需指定參數類型,也無需指定的重載形式——它們都將被自動推導。而如果使用方法引用,也是同樣可以根據上下文進行推導。
函數式接口是Lambda的基礎,而方法引用是Lambda的孿生兄弟。
下面這段代碼將會調用 println 方法的不同重載形式,將函數式接口改為int類型的參數:
@FunctionalInterface
public interface PrintableInteger {
/**
* 接收一個int類型參數,打印顯示它
* @param i int類型參數
*/
public abstract void print(int i);
}
由於上下文變了之后可以自動推導出唯一對應的匹配重載,所以方法引用沒有任何變化:
public class Demo03PrintOverload {
public static void main(String[] args) {
printInteger(System.out::println);
}
private static void printInteger(PrintableInteger data) {
data.print(1024);
}
}
這次方法引用將會自動匹配到 println(int) 的重載形式。
通過對象名引用成員方法
這是最常見的一種用法,與上例相同。如果一個類中已經存在了一個成員方法:
public class MethodRefObject {
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
函數式接口仍然定義為:
@FunctionalInterface
public interface Printable {
/**
* 接收一個字符串參數,打印顯示它
* @param str 字符串
*/
public abstract void print(String str);
}
拿到參數之后經Lambda之手,繼而傳遞給toUpperCase() 方法去處理:
public class Demo04MethodRef1 {
public static void main(String[] args) {
printString(s -> s.toUpperCase());
}
private static void printString(Printable lambda) {
lambda.print("Hello");
}
}
由於上面已經定義了printUpperCase()方法,且方法實現的功能就是:將傳入的字符串傳遞給toUpperCase()方法處理,然后打印輸出。
這個時候,當需要使用這個 printUpperCase 成員方法來替代 Printable 接口的Lambda的時候,已經具有了MethodRefObject 類的對象實例,則可以通過對象名引用成員方法,代碼為:
public class Demo04MethodRef2 {
public static void main(String[] args) {
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
}
private static void printString(Printable lambda) {
lambda.print("Hello");
}
}
運行程序,兩種方式的輸出是一樣的:
HELLO
通過類名稱引用靜態方法
由於在 java.lang.Math 類中已經存在了靜態方法 abs ,所以當我們需要通過Lambda來調用該方法時,有兩種寫法。首先是函數式接口:
@FunctionalInterface
public interface CalculationAble {
/**
* 接口傳入一個int類型的參數,經過自定義處理,返回一個int類型的數據
*/
int calculation(int num);
}
第一種寫法是使用Lambda表達式:
public class Demo5Lambda {
public static void main(String[] args) {
method(-666, n -> Math.abs(n));
}
private static void method(int num, CalculationAble lambda) {
System.out.println(lambda.calculation(num));
}
}
但是使用方法引用的更好寫法是:
public class Demo5MethodRef {
public static void main(String[] args) {
method(-666, Math::abs);
}
private static void method(int num, CalculationAble reference) {
System.out.println(reference.calculation(num));
}
}
在這個例子中,下面兩種寫法是等效的:
// Lambda表達式
n -> Math.abs(n)
// 方法引用
Math::abs
通過super引用成員方法
如果存在繼承關系,當Lambda中需要出現super調用時,也可以使用方法引用進行替代。首先是函數式接口:
@FunctionalInterface
public interface GreetAble {
/**
* 問候語
*/
void greet();
}
然后是父類 Human 的內容:
public class Human {
public void sayHello() {
System.out.println("Hello!");
}
}
最后是子類 Man 的內容,其中使用了Lambda的寫法:
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("大家好,我是Man!");
}
/**
* 定義方法method,參數傳遞GreetAble接口
* @param g 這里傳入的是Lambda表達式
*/
public void method(GreetAble g) {
g.greet();
}
/**
* 調用method方法,使用Lambda表達式
*/
public void show(){
// 創建Human對象,調用sayHello方法
method(() -> { new Human().sayHello(); });
// 簡化Lambda
method(() -> new Human().sayHello());
// 使用super關鍵字代替父類對象
method(() -> super.sayHello());
}
}
但是如果使用方法引用來調用父類中的 sayHello 方法會更好,例如另一個子類 Woman:
public class Woman extends Human {
@Override
public void sayHello() {
System.out.println("大家好,我是Man!");
}
/**
* 定義方法method,參數傳遞GreetAble接口
* @param g 引用方法
*/
public void method(GreetAble g) {
g.greet();
}
/**
* 調用method方法,使用引用方法
*/
public void show(){
method(super::sayHello);
}
}
在這個例子中,下面兩種寫法是等效的:
// Lambda表達式
() -> super.sayHello()
// 方法引用
super::sayHello
通過this引用成員方法
this代表當前對象,如果需要引用的方法就是當前類中的成員方法,那么可以使用“this::成員方法”的格式來使用方 法引用。首先是簡單的函數式接口:
@FunctionalInterface
public interface RichAble {
/**
* 買東西
*/
void buy();
}
下面是一個丈夫 Husband 類:
public class Husband01 {
/**
* 結婚
* @param lambda 函數式接口,買東西
*/
private void marry(RichAble lambda) {
lambda.buy();
}
/**
* 要開心
*/
public void beHappy() {
marry(() -> System.out.println("買套房子"));
}
}
開心方法 beHappy 調用了結婚方法 marry ,后者的參數為函數式接口 Richable ,所以需要一個Lambda表達式。 但是如果這個Lambda表達式的內容已經在本類當中存在了,則可以對 Husband 丈夫類進行修改:
public class Husband02 {
/**
* 買房子
*/
private void buyHouse() {
System.out.println("買套房子");
}
/**
* 結婚
* @param lambda 函數式接口,買東西
*/
private void marry(RichAble lambda) {
lambda.buy();
}
/**
* 要開心
*/
public void beHappy() {
marry(() -> this.buyHouse());
}
}
如果希望取消掉Lambda表達式,用方法引用進行替換,則更好的寫法為:
public class Husband03 {
/**
* 買房子
*/
private void buyHouse() {
System.out.println("買套房子");
}
/**
* 結婚
* @param lambda 函數式接口,買東西
*/
private void marry(RichAble lambda) {
lambda.buy();
}
/**
* 要開心
*/
public void beHappy() {
marry(this::buyHouse);
}
}
在這個例子中,下面兩種寫法是等效的:
// Lambda表達式
() -> this.buyHouse()
// 方法引用
this::buyHouse
類的構造器引用
由於構造器的名稱與類名完全一樣,並不固定。所以構造器引用使用 類名稱::new 的格式表示。首先是一個簡單 的 Person 類:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
然后是用來創建 Person 對象的函數式接口:
@FunctionalInterface
public interface PersonBuilder {
/**
* 創建 Person 對象
* @param name Person對象名
* @return Person對象
*/
Person buildPerson(String name);
}
要使用這個函數式接口,可以通過Lambda表達式:
public class Demo06Lambda {
public static void main(String[] args) {
printName("Lee Hua", (name) -> new Person(name));
}
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
}
但是通過構造器引用,有更好的寫法:
public class Demo07ConstructorRef {
public static void main(String[] args) {
printName("Lee Hua", Person::new);
}
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
}
在這個例子中,下面兩種寫法是等效的:
// Lambda表達式
name -> new Person(name)
// 方法引用
Person::new
數組也是 Object 的子類對象,所以同樣具有構造器,只是語法稍有不同。如果對應到Lambda的使用場景中時, 需要一個函數式接口:
@FunctionalInterface
public interface ArrayBuilder {
/**
* 創建數組的函數式接口
* @param length 數組長度
* @return 存儲int類型的數組
*/
int[] buildArray(int length);
}
在應用該接口的時候,可以通過Lambda表達式:
public class Demo08ArrayInitRef {
public static void main(String[] args) {
int[] array = initArray(10, length -> new int[length]);
}
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
}
但是更好的寫法是使用數組的構造器引用:
public class Demo09ArrayInitRef {
public static void main(String[] args) {
int[] array = initArray(10, int[]::new);
}
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
}
在這個例子中,下面兩種寫法是等效的:
// Lambda表達式
length -> new int[length]
// 方法引用
int[]::new
