開發與維運

方法引用的那些事兒

一句話介紹:

方法引用Method Reference)是在 Lambda 表達式的基礎上引申出來的一個功能。

先不鋪展概念,從一個示例開始說起。

一、小示例

List<Integer> list = Arrays.asList(1, 2, 3);
list.forEach(num -> System.out.println(num));

上面是一個很普通的 Lambda 表達式:遍歷打印列表的元素。

相比 JDK8 版本以前的 for  循環或 Iterator 迭代器方式,這種 Lambda 表達式的寫法已經是一種很精簡且易讀的改進。但有沒有更精簡的改進?

答案是有!下面就有請方法引用出場:

list.forEach(System.out::println);

沒用過這種方式的小夥伴,可能會納悶:這是什麼鬼?為什麼編譯器竟然不報錯?該怎麼理解?

這其實就是一種方法引用。中間的兩個冒號“::”,就是 Java 語言中方法引用的特有標誌,出現它,就說明使用到了方法引用。

因為 Foreach() 方法的形參是 Consume<T> 對象,所以,上面方法引用的方式等同於如下表達:

Consumer<Integer> consumer = System.out::print;
list.forEach(consumer);

有木有很神奇?System.out::print  語句的左值可以是一個 Consumer 對象。從編譯器的角度來理解,等號右側的語句是一種方法引用,那麼編譯器會認為該語句引用的是 Consumer 接口的 accept(T t) 抽象方法。

下面來細細拆分一下輸出語句:System.out.println();

System 是一個可不變類,包含了多個域變量和靜態方法,之所以能使用 System.out 這種形式,就因為 out 是它的一個靜態變量,且是一個  PrintStream  對象:

/**
 * The "standard" output stream. This stream is already
 * open and ready to accept output data. Typically this stream
 * corresponds to display output or another output destination
 * specified by the host environment or user.
 * <p>
 * For simple stand-alone Java applications, a typical way to write
 * a line of output data is:
 * <blockquote><pre>
 *     System.out.println(data)
 * </pre></blockquote>
 * <p>
 * See the <code>println</code> methods in class <code>PrintStream</code>.
 *
 * @see     java.io.PrintStream#println()
 * @see     java.io.PrintStream#println(boolean)
 * @see     java.io.PrintStream#println(char)
 * @see     java.io.PrintStream#println(char[])
 * @see     java.io.PrintStream#println(double)
 * @see     java.io.PrintStream#println(float)
 * @see     java.io.PrintStream#println(int)
 * @see     java.io.PrintStream#println(long)
 * @see     java.io.PrintStream#println(java.lang.Object)
 * @see     java.io.PrintStream#println(java.lang.String)
*/
public final static PrintStream out = null;

println(xxx)PrintStream 類裡一個普通方法。println(xxx) 方法有多個重載,不同點在入參的類型,可以使 int、float、double、char、char[]、boolean、long 等。

public void println(T x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

前面囉嗦那麼多,重點來了!

println(xxx)  方法的特點是隻有一個入參,沒有出參。這個和 Consumer<T>  函數式接口的 accept(T t)  是不是很像?這也是方法引用的精髓:

只要一個已存在的方法,其入參類型、入參個數和函數式接口的抽象方法相同(不考慮兩者的返回值),就可以使用該方法(如本例中的 println(xxx)),來指代函數式接口的抽象方法(如本例中的  accept(T t) 方法),等於是該抽象方法的一種實現,也不需要繼承該函數式接口。

直接用已存的類名 + 兩個冒號 + 方法名即可:類名::方法名。注意,這裡的方法名是不帶括號的。

這個比 Lambda 表達式還省事,Lambda 表達式是在不繼承接口的基礎上,直接用形如  () -> {} 的方式變相實現了抽象方法,方法引用是直接用已存的方法來指代該抽象方法!

總結一下,方法引用解決了什麼問題?

它解決了代碼功能複用的問題,使得表達式更為緊湊,可讀性更強,藉助已有方法來達到傳統方式下需多行代碼才能達到的目的。

二、方法引用的語法

方法引用的語法很簡單。

使用一對冒號 :: 來完成,分為左右兩個部分,左側為類名或對象名,右側為方法名或 new 關鍵字。有以下四種類型:

## 方法引用的幾種類型:
1、構造器引用,形式為 類名::new
2、靜態方法引用,形式為 類名::方法名
3、類特定對象的方法引用,形式為 類對象::方法名
4、類的任意對象引用,形式為 類名::方法名

看個非常簡單的示例,對應了上面的四種引用類型。

public class Animal {
    private String name;

    public Animal() {
    }

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public static Animal getInstance(Supplier<Animal> supplier) {
        return supplier.get();
    }

    public void guard(Animal animal) {
        System.out.println(this.getName() + " guard " + animal.getName());
    }

    public void sleep() {
        System.out.println(this.getName() + " sleep.");
    }

    public static void bodyCheck(Animal animal) {
        System.out.println("body check " + animal.getName());
    }
}

定義了一個簡單的 Animal 類,包含了靜態方法、普通方法、有參構造函數等。

接下來,我們看下基於這個  Animal 類,四種方法引用類型的使用:

public static void main(String[] args) {
    List<Animal> animalList = new ArrayList<Animal>() {{
        add(new Animal("sheep"));
        add(new Animal("cow"));
    }};

    System.out.println("---- 構造器引用 ----");
    Animal pig = Animal.getInstance(Animal::new);
    pig.sleep();

    System.out.println("\n---- 靜態對象的引用 ----");
    animalList.forEach(Animal::bodyCheck);

    System.out.println("\n---- 類特定對象的引用 ----");
    Animal dog = new Animal("dog");
    animalList.forEach(dog::guard);

    System.out.println("\n---- 類的任意對象的引用 ----");
    animalList.forEach(Animal::sleep);
}

如果上面的代碼你都理解了,那方法引用你也已經基本掌握了。

下面,針對方法引用的這幾種類型,各自再詳細解釋。

三、方法引用的幾種類型

3.1 構造器引用

語法很簡單:類名::方法名,使用方式如下:

// 示例 1
Supplier<List<Integer>> supplier1 = ArrayList::new;
List<Integer> list = supplier1.get();

// 示例 2
Supplier<Animal> supplier2 = Animal::new;
Animal animal = supplier2.get();

之所以能賦值給 Supplier 接口,是因為其抽象方法 get() 沒有入參,與類的無參構造函數一致。

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

這裡還需要注意一點,自定義的類必須有“無參構造函數”,否則編譯器會報錯

我們都知道,當創建一個類後,如果不顯式聲明構造函數,編譯器會默認加一個無參構造函數。但如果有顯式聲明一個或多個有參構造函數,則編譯器不再默認追加無參構造函數。如下:

public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }
}

上面代碼中的 Animal 類只有一個構造函數  Animal(String name),不再有無參構造函數。這種方式下使用構造器引用就會報錯:

Supplier<Animal> supplier = Animal::new; // 編譯報錯:Cannot resolve constructor 'Animal'

3.2 靜態方法引用

語法為 類名::靜態方法名

還是以上面的 Animal 類為例,為了更好展示靜態方法引用,相比上面的示例,我們適當做一下調整:

public class Animal {
    private String name;
    private Integer weight;

    public Animal() {
    }

    public Animal(String name, Integer weight) {
        this.name = name;
        this.weight = weight;
    }

    public String getName() {
        return name;
    }

    public Integer getWeight() {
        return weight;
    }

    ...

    public static void bodyCheck(Animal animal) {
        System.out.println("body check " + animal.getName());
    }

    public static Integer compareByName(Animal one, Animal another) {
        return one.getName().compareTo(another.getName());
    }

    public Integer compareByWeight(Animal one, Animal another) {
        return one.getWeight() - another.getWeight();
    }
}

Animal 類有兩個成員變量 nameweight,它有多個方法,其中包括兩個靜態方法  compareByName() 和  compareByWeight()

給定一個 Animal  對象列表,如果我們想根據名稱排序,可以怎麼做?你想到了幾種方式?

  • 第一種:利用 Collections.sort(List<T> list) 方法

這種方式,需要 Animal 類實現 Coparable<T> 接口,給出  compareTo(T t)  抽象方法的具體實現,如下所示:

public class Animal implements Comparable {

    ...

    @Override
    public int compareTo(Object o) {
        Animal another = (Animal)o;
        return this.getName().compareTo(another.toString());
    }
}

// 調用
Collections.sort(animalList);

這種方式在 JDK7 版本及以前使用的比較多。

  • 第二種:利用  Collections.sort(List<T> list, Comparator<? super T> c) 方法

在集合類  Collections<T>  中,還有一個 sort(List<T> list) 的重載方法  sort(List list, Comparator<? super T> c)

使用該方法,Animal 類就無需再實現 Comparable<T> 接口,在 JDK7 版本及以前,使用匿名內部類來調用此方法即可。

相比第一種方式,結構上輕便了很多,代碼實現如下:

Collections.sort(animalList, new Comparator<Animal>() {
    @Override
    public int compare(Animal o1, Animal o2) {
        return o1.getName().compareTo(o2.getName());
    }
});
  • 第三種:利用 Lambda 表達式

和第二種類似,只不過隨著  JDK8 版本中 Lambda 表達式的出現,可替換以往的匿名內部類,代碼實現上做到更簡潔:

// Lambda 表達式的實現
Collections.sort(animalList, (a, b) -> a.getName().compareTo(b.getName()));
  • 第四種:藉助方法引用

在第一種方式中,Animal 類還要實現 Comparable<T> 接口,然後做  compare()  抽象方法的具體實現。

整個實現上是過於笨重的,太形式化。

有了方法引用,就可以大大減輕這種不必要的形式化。因為 Animal 類中已經有了類似的比較方法,即靜態方法  compareByName()

直接用這個方法代替 compare() 方法不就行啦,如下:

Collections.sort(animalList, Animal::compareByName);

是不是很簡單!沒有接口實現,也沒有匿名內部類,以一種優雅的方式達到了相同的目的,這也是方法引用的魅力之處。

我個人理解,方法引用的出現,就是為了去優化冗餘且過於形式化的代碼,直接用短平快的方式解決。

  • 第五種:利用 List 接口的 sort() 默認方法

除了 Collections 集合類,List 接口中,也提供了列表的排序方法。

// 匿名內部類實現
animalList.sort(new Comparator<Animal>() {
    @Override
    public int compare(Animal o1, Animal o2) {
        return o1.getName().compareTo(o2.getName());
    }
});

// Lambda 表達式實現
animalList.sort((a, b) -> a.getName().compareTo(b.getName()));

// 靜態方法引用的實現
animalList.sort(Animal::compareByName);
  • 第六種:Stream() 流排序

Stream() 流是 JDK8 中新引入的功能,排序代碼如下:

// 方式 1:Lambda 表達式實現
animalList = animalList
    .stream()
    .sorted((a, b) -> a.getName().compareTo(b.getName()))
    .collect(Collectors.toList());

// 方式 2:靜態方法引用
animalList = animalList
    .stream()
    .sorted(Animal::compareByName)
    .collect(Collectors.toList());

3.3 類特定對象的引用

在前一章節的第五種方式中,我們可以替換為類特定對象的引用。

語法:類對象::普通方法名

在上面的 Animal 類中,有一個普通方法:

public Integer compareByWeight(Animal one, Animal another) {
    return one.getWeight() - another.getWeight();
}

compareByWeight() 就是一個普通的實例方法,但它的定義依然與 Comparable 接口的 compare()  抽象方法定義是一致的。所以也可以使用在方法引用中。

怎麼使用呢?方式如下:

Animal dog = new Animal("dog", 40);
animalList.sort(dog::compareByWeight);

類特定對象的引用、靜態方法引用,兩者在使用上沒有區別,都達到一樣的目的,只是方式不同,一個是類 + 靜態方法名,一個是類對象 + 普通方法名。

3.4 類的任意對象的引用

語法:類名::普通方法名

從語法上看,與前面  2.3.2 小節的靜態方法引用類似,都是類名 + 方法名的方式,只不過一個是普通方法,一個是靜態方法,但這是不是意味著兩者在含義上也是類似的呢?

答案是否定的。

對於  2.3.2 章節的靜態方法引用,以及 2.3.3 章節的類特定對象的引用,它們的重點都是在引出方法,只不過引出的方式不同。

public class Animal {
    private String name;
    private Integer weight;

    public Animal(String name, Integer weight) {
        this.name = name;
        this.weight = weight;
    }

    ...

    public static Integer compareByName(Animal one, Animal another) {
        return one.getName().compareTo(another.getName());
    }

    public Integer compareByWeight(Animal one, Animal another) {
        return one.getWeight() - another.getWeight();
    }
}


// 靜態方法引用
animalList.sort(Animal::compareByName);

// 類的特定對象的引用
Animal dog = new Animal("dog", 40);
animalList.sort(dog::compareByWeight);

就像上面的代碼中,“類的特定對象的引用”示例中,換個 Animal 對象,依然能達到同樣的效果:

Animal cat = new Animal("cat", 15);
animalList.sort(cat::compareByWeight);

好了,現在回到本小節的主題:類的任意對象的引用

我們可以怎麼用呢?

在繼續講之前,我們先回頭再觀察下前面面代碼中的 compareByWeight(xx, xxx) 方法。有沒有發現它的兩個參數有點兒冗餘?另外,如果是兩個參數,這個方法放在任何一個類中都可以使用,完全可以把它抽到一個工具類中使用,沒必要放在這個類中。如果要放在該類中,可以換一種方式,傳遞一個參數即可:

public Integer compareByWeight(Animal another) {
    return this.getWeight() - another.getWeight();
}

調用代碼如下:

animalList.sort(Animal::compareByWeight);

這裡很多人都會疑惑,方法引用的前提,不都是入參個數都要一樣嗎?但 compareByWeight(Animal another) 方法只有一個參數,而 sort()  方法的形參  Comparator<T> 對應的抽象方法  compare(T o1, T o2) 是兩個參數:

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

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

這就是“類的任意對象的引用”這種類型的特殊之處。

方法引用會默認將第一個入參作為當前類的一個調用對象,其餘參數繼續作為方法的入參。

在本例中,compare(T o1, T o2) 方法是需要接入兩個 Animal 對象的,但第一個對象 o1 可以作為當前 Animal 類的一個對象,剩下的 o2 繼續作為引用方法  compareByWeight()  的參數,即:

o1.compareByWeight(o2)

這也是為何稱為“類的任意對象的引用”。

為加深理解,我們再舉一個例子。

前面的 Animal 類中,有一個 sleep()  普通方法和 bodyCheck(xx)  靜態方法:

public class Animal {
    private String name;
    ...
    public void sleep() {
        System.out.println(this.getName() + " sleep.");
    }
    public static void bodyCheck(Animal animal) {
        System.out.println("body check " + animal.getName());
    }
}

Animal::sleep  構成了“類的任意對象的引用”,Animal::bodyCheck  構成了“靜態方法引用”,它們都可以用在如下表達式中:

animalList.forEach(Animal::sleep);
animalList.forEach(Animal::bodyCheck);

sleep() 方法雖然沒有入參,但依然可以用在 forEach() 方法中,因為  Consumer<T>  接口的 accept(T t) 抽象方法有一個入參,而該入參就可以作為 Animal 類的一個對象,來調用 sleep() 方法。

四、總結

如上所述,方法引用有多種類型,在實際使用過程中,可靈活運用。

說到底,跟 Lambda 表達式一樣,它還是一種語法糖,為我們的開發工作提效。為達到同樣的目標,相比傳統實現方式,這種語法糖減輕了代碼量,使用更輕便,不再拘泥於特定場景下囿於面嚮對象語言規則而產生的笨重表達,是對它們的一種輕量級替代。

Leave a Reply

Your email address will not be published. Required fields are marked *