java8新特性 lambda表達式升級 方法引用

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章