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