理解 Java 方法引用(方法引用符:“雙冒號 :: ”)


方法引用

在使用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM