一、概述
什麼是方法引用?我們首先來看一個例子:
在學習lambda表達式之後,我們通常使用lambda表達式來創建匿名方法。然而,有時候我們僅僅是調用了一個已存在的方法。比如:
Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));
其中,compareToIgnoreCase是String類中已經存在的方法,所以我們也可以用方法引用的格式來寫:
Arrays.sort(stringsArray, String::compareToIgnoreCase);
String::compareToIgnoreCase就是一種方法引用(Method Reference)。
二、什麼是方法引用
方法引用是用來直接訪問類或者實例的已經存在的方法或者構造方法。當Lambda表達式中只是執行一個方法調用時,不用Lambda表達式,直接通過方法引用的形式可讀性更高一些。方法引用是一種更簡潔易懂的Lambda表達式。方法引用提供了一種引用而不執行方法的方式,它需要由兼容的函數式接口構成的目標類型上下文。計算時,方法引用會創建函數式接口的一個實例(因爲Lambda會創建函數式接口的實例)。
簡單來說, 就是一個Lambda表達式。在Java 8中,我們會使用Lambda表達式創建匿名方法,但是有時候,我們的Lambda表達式可能僅僅調用一個已存在的方法,而不做任何其它事,對於這種情況,通過一個方法名字來引用這個已存在的方法會更加清晰,Java 8的方法引用允許我們這樣做。方法引用是一個更加緊湊,易讀的Lambda表達式,注意方法引用是一個Lambda表達式,其中方法引用的操作符是雙冒號"::"。
注意方法引用是一個Lambda表達式,其中方法引用的操作符是雙冒號"::"。
三、方法引用例子
先看一個例子,首先定義一個Person類,如下:
package com.demo.model;
import java.time.LocalDate;
public class Person {
public Person(String name, LocalDate birthday) {
this.name = name;
this.birthday = birthday;
}
String name;
LocalDate birthday;
public LocalDate getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
@Override
public String toString() {
return this.name;
}
}
假設我們有一個Person數組,並且想對它進行排序,這時候,我們可能會這樣寫:
原始寫法,使用匿名類:
package com.demo;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;
import org.junit.Test;
import com.demo.model.Person;
public class testMethodReference {
@Test
public void test() {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
// 使用匿名類
Arrays.sort(pArr, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
});
System.out.println(Arrays.asList(pArr));
}
}
其中,Arrays類的sort方法定義如下:
public static <T> void sort(T[] a, Comparator<? super T> c)
這裏,我們首先要注意Comparator
接口是一個函數式接口,因此我們可以使用Lambda表達式,而不需要定義一個實現Comparator
接口的類,並創建它的實例對象,傳給sort方法。
使用Lambda表達式,我們可以這樣寫:
改進一,使用Lambda表達式,未調用已存在的方法
@Test
public void test1() {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
//使用lambda表達式
Arrays.sort(pArr, (Person a, Person b) -> {
return a.getBirthday().compareTo(b.getBirthday());
});
System.out.println(Arrays.asList(pArr));
}
然而,在以上代碼中,關於兩個人生日的比較方法在Person類中已經定義了,因此,我們可以直接使用已存在的Person.compareByAge方法。
改進二,使用Lambda表達式,調用已存在的方法
@Test
public void test2() {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
//使用lambda表達式和類的靜態方法
Arrays.sort(pArr, (a ,b) -> Person.compareByAge(a, b));
System.out.println(Arrays.asList(pArr));
}
因爲這個Lambda表達式調用了一個已存在的方法,因此,我們可以直接使用方法引用來替代這個Lambda表達式。
改進三,使用方法引用
@Test
public void test3() {
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
//使用方法引用,引用的是類的靜態方法
Arrays.sort(pArr, Person::compareByAge);
System.out.println(Arrays.asList(pArr));
}
運行結果:
[001, 002, 003, 004]
在以上代碼中,方法引用Person::compareByAge在語義上與Lambda表達式 (a, b) -> Person.compareByAge(a, b) 是等同的,都有如下特性:
- 真實的參數是拷貝自Comparator<Person>.compare方法,即(Person, Person);
- 表達式體調用Person.compareByAge方法。
四、四種方法引用類型
方法引用的標準形式是:類名::方法名
。(注意:只需要寫方法名,不需要寫括號)
有以下四種形式的方法引用:
類型 | 示例 |
引用靜態方法 | ContainingClass::staticMethodName |
引用某個對象的實例方法 | containingObject::instanceMethodName |
引用某個類型的任意對象的實例方法 | ContainingType::methodName |
引用構造方法 | ClassName::new |
學習這幾種形式的方法引用:
1、靜態方法引用
組成語法格式:ClassName::staticMethodName
我們前面舉的例子Person::compareByAge就是一個靜態方法引用。
注意:
- 靜態方法引用比較容易理解,和靜態方法調用相比,只是把 . 換爲 ::
- 在目標類型兼容的任何地方,都可以使用靜態方法引用。
例子:
String::valueOf 等價於lambda表達式 (s) -> String.valueOf(s)
Math::pow 等價於lambda表達式 (x, y) -> Math.pow(x, y);
字符串反轉的例子:
package com.demo;
/**
* 函數式接口
*/
public interface StringFunc {
String func(String n);
}
package com.demo;
public class MyStringOps {
//靜態方法: 反轉字符串
public static String strReverse(String str) {
String result = "";
for (int i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
}
}
package com.demo;
public class MethodRefDemo {
public static String stringOp(StringFunc sf, String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr = "lambda add power to Java";
//MyStringOps::strReverse 相當於實現了接口方法func()
// 並在接口方法func()中作了MyStringOps.strReverse()操作
String outStr = stringOp(MyStringOps::strReverse, inStr);
System.out.println("Original string: " + inStr);
System.out.println("String reserved: " + outStr);
}
}
輸出結果:
Original string: lambda add power to Java
String reserved: avaJ ot rewop dda adbmal
表達式MyStringOps::strReverse的計算結果爲對象引用,其中,strReverse提供了StringFunc的func()方法的實現。
找到列表中具有最大值的對象
package com.demo;
public class MyClass {
private int val;
MyClass(int v) {
val = v;
}
public int getValue() {
return val;
}
}
package com.demo;
import java.util.ArrayList;
import java.util.Collections;
public class UseMethodRef {
public static int compareMC(MyClass a, MyClass b) {
return a.getValue() - b.getValue();
}
public static void main(String[] args) {
ArrayList<MyClass> a1 = new ArrayList<MyClass>();
a1.add(new MyClass(1));
a1.add(new MyClass(4));
a1.add(new MyClass(2));
a1.add(new MyClass(9));
a1.add(new MyClass(3));
a1.add(new MyClass(7));
//UseMethodRef::compareMC生成了抽象接口Comparator定義的compare()方法的實例。
MyClass maxValObj = Collections.max(a1, UseMethodRef::compareMC);
System.out.println("Maximum value is: " + maxValObj.getValue());
}
}
輸出結果:
Maximum value is: 9
UseMethodRef定義了靜態方法compareMC(),它與Comparator定義的compare()方法兼容。因此,沒有必要顯式的實現Comparator接口並創建其實例。
2、特定實例對象的方法引用
這種語法與用於靜態方法的語法類似,只不過這裏使用對象引用而不是類名。實例方法引用又分以下三種類型:
a.實例上的實例方法引用
組成語法格式:instanceReference::methodName
如下示例,引用的方法是myComparisonProvider 對象的compareByName方法。
class ComparisonProvider{
public int compareByName(Person a, Person b){
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b){
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
例子:反轉字符串
package com.demo;
/**
* 函數式接口
*/
public interface StringFunc {
String func(String n);
}
package com.demo;
public class MyStringOps {
//普通方法: 反轉字符串
public String strReverse1(String str) {
String result = "";
for (int i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
}
}
package com.demo;
public class MethodRefDemo2 {
public static String stringOp(StringFunc sf, String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr = "lambda add power to Java";
MyStringOps strOps = new MyStringOps();//實例對象
//strOps::strReverse1 相當於實現了接口方法func()
String outStr = stringOp(strOps::strReverse1, inStr);
System.out.println("Original string: " + inStr);
System.out.println("String reserved: " + outStr);
}
}
輸出結果:
Original string: lambda add power to Java
String reserved: avaJ ot rewop dda adbmal
b.超類上的實例方法引用
組成語法格式:super::methodName
方法的名稱由methodName指定,通過使用super,可以引用方法的超類版本。
還可以捕獲this 指針,this :: equals 等價於lambda表達式 x -> this.equals(x);
c.類型上的實例方法引用
組成語法格式:ClassName::methodName
注意:
若類型的實例方法是泛型的,就需要在::分隔符前提供類型參數,或者(多數情況下)利用目標類型推導出其類型。
靜態方法引用和類型上的實例方法引用擁有一樣的語法。編譯器會根據實際情況做出決定。一般我們不需要指定方法引用中的參數類型,因爲編譯器往往可以推導出結果,但如果需要我們也可以顯式在::分隔符之前提供參數類型信息。
例子:
String::toString 等價於lambda表達式 (s) -> s.toString()
這裏不太容易理解,實例方法要通過對象來調用,方法引用對應Lambda,Lambda的第一個參數會成爲調用實例方法的對象。
在泛型類或泛型方法中,也可以使用方法引用。
package com.demo;
public interface MyFunc<T> {
int func(T[] als, T v);
}
package com.demo;
public class MyArrayOps {
public static <T> int countMatching(T[] vals, T v) {
int count = 0;
for (int i = 0; i < vals.length; i++) {
if (vals[i] == v) count++;
}
return count;
}
}
package com.demo;
public class GenericMethodRefDemo {
public static <T> int myOp(MyFunc<T> f, T[] vals, T v) {
return f.func(vals, v);
}
public static void main(String[] args){
Integer[] vals = {1, 2, 3, 4, 2, 3, 4, 4, 5};
String[] strs = {"One", "Two", "Three", "Two"};
int count;
count=myOp(MyArrayOps::<Integer>countMatching, vals, 4);
System.out.println("vals contains "+count+" 4s");
count=myOp(MyArrayOps::<String>countMatching, strs, "Two");
System.out.println("strs contains "+count+" Twos");
}
}
運行結果:
vals contains 3 4s
strs contains 2 Twos
分析:
在程序中,MyArrayOps是非泛型類,包含泛型方法countMatching()。該方法返回數組中與指定值匹配的元素的個數。注意這裏如何指定泛型類型參數。例如,在main()方法中,對countMatching()方法的第一次調用如下所示:count = myOp(MyArrayOps::<Integer>countMatching,vals,4); 這裏傳遞了類型參數Integer。
注意,參數傳遞發生在::的後面。這種語法可以推廣。當把泛型方法指定爲方法引用時,類型參數出現在::之後、方法名之前。但是,需要指出的是,在這種情況(和其它許多情況)下,並非必須顯示指定類型參數,因爲類型參數會被自動推斷得出。對於指定泛型類的情況,類型參數位於類名的後面::的前面。
3、任意對象(屬於同一個類)的實例方法引用
如下示例,這裏引用的是字符串數組中任意一個對象的compareToIgnoreCase方法。
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
注意:這裏的compareToIgnoreCase方法不是靜態方法,所以正常調用需要實例化對象才能調用。故這裏體現的就是,引用的是字符串數組中任意一個對象的compareToIgnoreCase方法。
4、構造方法引用
構造方法引用又分構造方法引用和數組構造方法引用。
a.構造方法引用(也可以稱作構造器引用)
組成語法格式:Class::new
構造函數本質上是靜態方法,只是方法名字比較特殊,使用的是new 關鍵字。
例子:
String::new, 等價於lambda表達式 () -> new String()
package com.demo;
public interface MyFunc1 {
MyClass func(int n);
}
package com.demo;
public class MyClass {
private int val;
MyClass(int v) {
val = v;
}
MyClass(){
val = 0;
}
public int getValue() {
return val;
}
}
package com.demo;
public class ConstructorRefDemo {
public static void main(String[] args) {
MyFunc1 myClassCons = MyClass :: new;
MyClass mc = myClassCons.func(100);
System.out.println("val in mc is: " + mc.getValue());
}
}
輸出結果:
val in mc is: 100
b.數組構造方法引用
組成語法格式:TypeName[]::new
例子:
int[]::new 是一個含有一個參數的構造器引用,這個參數就是數組的長度。等價於lambda表達式 x -> new int[x]。
假想存在一個接收int參數的數組構造方法(函數式接口):
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 創建數組 int[10]