1. 程式人生 > >淺談Java 8中的方法引用(Method References)

淺談Java 8中的方法引用(Method References)

  本人接觸Java 8的時間不長,對Java 8的一些新特性略有所知。Java 8引入了一些新的程式設計概念,比如經常用到的 lambda表示式、Stream、Optional以及Function等,讓人耳目一新。這些功能其實上手並不是很難,根據別人的程式碼抄過來改一下,並不要知道內部的實現原理,也可以很熟練地用好這些功能。但是當我深究其中一些細節時,會發現有一些知識的盲區。下面我就來談一下Java 8中的Method References這個概念。

  首先我給出官方對於這一概念的詳細解釋,https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

。本文雖不是簡單的翻譯官方文件,但是還是有必要簡要的介紹一下這一概念。

1. 什麼是方法引用(Method References)

   方法引用(Method References)是一個與Lambda表示式、函式式介面(Functional Inferface)緊密關聯的概念。如我們所知,函式式介面(Functional Inferface)是一種有且僅有一個抽象方法的介面。而Lambda表示式是Java 8引入的對於函式式介面更加簡潔的實現方式。方法引用(Method References)則是另一種對函式式介面的實現方式。下面是Java 8中的一個函式式介面(Functional Inferface)的一般性定義,它可以擁有一個或多個default 方法和 static方法,但只能擁有一個抽象方法(這裡abstract關鍵字可以被省略)。

/** 
*這就是一個Functional Interface,無論加不加註解@FunctionalInterface,這都是一個Functional Interface。
*/
public interface A {
  public abstract void method1(int a);

  public default void method2(int b) {
  //Do something
  };
  public static void method3(int c) {    
  //Do something
  };
}

    如果有其他的方法需要以 Interface A的例項作為入參時,在Lambda表示式出現之前,我們一般會使用匿名內部類的方式來處理。如下所示:

public class B {
//類B的某一個方法的入參需要傳入介面A的一個例項
public void method1(A a) { a.method1(1); a.method2(2); } public static void main(String args[]){ B b = new B(); //使用匿名內部類實現函式式介面A的唯一抽象方法,並傳入例項 b.method1(new A() { @Override public void method1(int a) { //Do something } }); } }

    Lambda表示式出現以後,我們開始使用下面這種方式:

public class B {
//類B的某一個方法的入參需要傳入介面A的一個例項
public void method1(A a) { a.method1(1); a.method2(2); } public static void main(String args[]){ B b = new B(); //使用Lambda表示式實現函式式介面的唯一抽象方法 b.method1( (a)->{ /*Do something*/ }); } }

    使用方法引用(Method References),可以將上面的程式碼轉換為如下程式碼:

public class B {
    //類B的某一個方法的入參需要傳入介面A的一個例項
    public void method1(A a) {
        a.method1(1);
        a.method2(2);
    }
    //類B的兩個靜態方法
    public static void method2(int a) {
        //Do something
    }
    public static int method3(int a) {
        //Do something
        return 1;
    }
    public static void main(String args[]){
        B b = new B();
        //使用匿名內部類實現函式式介面的唯一抽象方法
        b.method1((a)->{/*Do something*/});
        b.method1(B::method2);
        b.method1(B::method3);//由於介面中的方法返回型別是void,此處會丟棄麼B::method3的返回值
  } 
}

    這種用類名加兩個冒號的寫法,就是方法引用(Method References)。有點類似於C語言的函式指標,將一個方法作為另一個方法的入參。在面嚮物件語言中,這是一種將方法物件化的方式。在我們已經有現成的方法時,不需要再去實現一遍這個方法,只需要把現有的方法視為一個物件,將它的引用作為入參,比Lambda表示式還要方便快捷。

2. 方法引用的種類

    官方的文件給出了4中型別的方法引用:

Kind Example
Reference to a static method ContainingClass::staticMethodName
Reference to an instance method of a particular object containingObject::instanceMethodName
Reference to an instance method of an arbitrary object of a particular type ContainingType::methodName
Reference to a constructor ClassName::new

    Reference to a static method就是我們上面程式碼中所示,將一個static method作為方法引用。

    Reference to an instance method of a particular object也很好理解,當我們需要使用某個方法時,發現它不是static method,這時我們需要先生成一個擁有這個方法的物件例項,然後通過例項的引用識別符號去呼叫這個方法:

public class B {
    //類B的某一個方法的入參需要傳入介面A的一個例項
    public void method1(A a) {
        a.method1(1);
        a.method2(2);
    }
    //類B的兩個靜態方法
    public static void method2(int a) {
        //Do something
    }
    public static int method3(int a) {
        //Do something
        return 1;
    }
//類B的兩個成員方法
public void method4(int a) { //Do something } public int method5(int a) { //Do something return 1; } public static void main(String args[]){ B b = new B(); //使用匿名內部類實現函式式介面的唯一抽象方法 b.method1((a)->{/*Do something*/}); b.method1(B::method2); b.method1(B::method3);//由於介面中的方法返回型別是void,此處會丟棄B::method3的int返回值 B anotherB=new B(); b.method1(anotherB::method4); b.method1(anotherB::method5);//同上,由於介面中的方法返回型別是void,此處會丟棄anotherB::method5的int返回值
  } 
}

     大家應該注意到了,這裡有一個比較特殊的處理,雖然介面A中的方法method1的返回型別為void,但仍然可以傳入一個返回型別為int的方法引用。如果介面A的返回型別為int,方法引用的返回引數可以是byte,但不能是long。這裡大家如果感興趣可以繼續深入的研究。    

    Reference to an instance method of an arbitrary object of a particular type,這個相對複雜一點,官方文件也一筆帶過了,這裡我們再深入一點。首先我們先分析官方文件中的例子:

The following is an example of a reference to an instance method of an arbitrary object of a particular type:

String[] stringArray = { "Barbara", "James", "Mary", "John",
    "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

The equivalent lambda expression for the method reference String::compareToIgnoreCase would have the formal parameter list (String a, String b), where a and bare arbitrary names used to better describe this example. The method reference would invoke the method a.compareToIgnoreCase(b).

  這裡需要先知道Arrays.sort和Comparator的程式碼大概做了什麼:

public class Arrays {
//..........
      public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
//.............
}

  這裡相當於將String::compareToIgnoreCase作為方法引用去實現Comparator介面,那麼Comparator介面中有什麼呢?其實就是下面程式碼中描述的:

@FunctionalInterface
public interface Comparator<T> {
        int compare(T o1, T o2);
}    

  而String中的compareToIgnoreCase卻只有一個入參,與Comparator中的compare方法引數列表不一致:

Class String{
//......    
    public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }
//.......
}

  這時在使用Arrays.sort(stringArray, String::compareToIgnoreCase);時 sort方法中是不知道傳過來的是String::compareToIgnoreCase的,依然會使用類似c.compare(stringArray[i],stringArray[j])的語句去比較字串。但這時實際執行的是stringArray[i].compareToIgnoreCase(stringArray[j])。這是官方的一個例子,下面我們來自己寫一個更為普遍一些的例子。

   如下所示,介面A中的method1有兩個入參,第一個入參為class B的例項,在main方法中我們向B.method1中傳遞了一個方法引用B::method2。我們已經知道,如果method2是B中的靜態方法,我們可以使用B::method2,否則我們只能先new一個B的例項,比如 B b=new B(); 然後使用b::method2。這裡method2不是一個靜態方法,但是我們仍然使用了B::method2,為什麼呢?這就是方法引用的Reference to an instance method of an arbitrary object of a particular type。這時在B.method1中,並不知道引數a具體是通過什麼方式實現的,有可能是用匿名內部類,有可能是lambda表示式,有可能是其他類的靜態方法等等。所以在B.method1中只能使用介面A宣告的方式去呼叫。但是實際上,Java 8在這裡進行了處理,對方法引用進行了轉換,介面方法中的第一個引數,對映為例項的引用,第二個引數才是這個例項的方法中的引數。方法引用--這一概念最好的體現就在於此,將B::method2賦值給介面A的一個引用,即使引數個數不同也可以賦值。

@FunctionalInterface
public interface A {
    public abstract int method1(B b,int a);
}

public class B {
    public static void method1(A a) {
        a.method1(new B(),1);
    }
    
    public int method2(int a) { return 1;}
    public static void main(String args[]){
        B.method1(B::method2);
    }
}

     Reference to a constructor,這種方法引用的特殊之處在於使用ClassName::new來表示建構函式。當然,官方文件中已經解釋的很好了,我這裡僅做一下概括。如下所示,一般的文章會把B.method1(B::new)等同於lambda表示式B.method1( ()->{return new B()} ), 但在下面的例子中,上述轉換無法編譯。因為介面A中的抽象方法method1的返回值為void。但這時仍可以使用B::new,也可以正常打印出1024。

@FunctionalInterface
public interface A {
    public abstract void method1(int a);
}

public class B {
    int value;
    public static void method1(A a) {
         a.method1(1024);
    }
    public B(int value) {
        this.value=value;
        System.out.println(value);
    }
    public static void main(String args[]){
        B.method1(B::new);
  B.method1( ()->{return new B()} ) //compile error } }

結語

   方法引用(Method References)的上述4種使用場景十分靈活,與lambda表示式、函式式介面(Functional Inferface)共同組成了Java對於方法物件化的實現。在此基礎上又擴展出了Java 8的Function包中的眾多類,而Function包又對Stream包等一系列包提供了強大的支援。Java8 的程式設計因此變得更為靈活。