一句話介紹:
方法引用(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
類有兩個成員變量 name
和 weight
,它有多個方法,其中包括兩個靜態方法 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 表達式一樣,它還是一種語法糖,為我們的開發工作提效。為達到同樣的目標,相比傳統實現方式,這種語法糖減輕了代碼量,使用更輕便,不再拘泥於特定場景下囿於面嚮對象語言規則而產生的笨重表達,是對它們的一種輕量級替代。