方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。
在 Java 8 中,我们会使用 Lambda 表达式创建匿名方法,但是有时候,我们的 Lambda 表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8 的方法引用允许我们这样做。方法引用是一个更加紧凑,易读的 Lambda 表达式,注意方法引用是一个 Lambda 表达式,其中方法引用的操作符是双冒号”::”。
方法引用 Person::compareByAge 在语义上与 Lambda 表达式 (a, b) -> Person.compareByAge(a, b) 是等同的,都有如下特性:
真实的参数是拷贝自 Comparator
There are four kinds of method references:
Kind | Syntax | Examples |
---|---|---|
引用静态方法 | ContainingClass::staticMethodName | Person::compareByAge MethodReferencesExamples::appendStrings |
引用某个对象的实例方法 | containingObject::instanceMethodName | myComparisonProvider::compareByName myApp::appendStrings2 |
引用某个类型的任意对象的实例方法 | ContainingType::methodName | String::compareToIgnoreCase String::concat |
引用构造方法 | ClassName::new | HashSet::new |
import java.util.function.BiFunction;
/**
* @author clk.weng
* @date 2022/12/8 19:47
*/
public class MethodReferencesExamples {
public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) {
return merger.apply(a, b);
}
public static String appendStrings(String a, String b) {
return a + b;
}
public String appendStrings2(String a, String b) {
return a + b;
}
public static <T, U, R> R particularTypeArbitraryObj(T a, U b, BiFunction<T, U, R> funInterface) {
return funInterface.apply(a, b);
}
public static void main(String[] args) {
// Calling the method mergeThings with a lambda expression
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (a, b) -> a + b));
// 引用静态方法
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings));
//等价:
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (String a, String b) -> MethodReferencesExamples.appendStrings(a, b)));
// 引用某对象实例方法
MethodReferencesExamples myApp = new MethodReferencesExamples();
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", myApp::appendStrings2));
//等价:
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (String a, String b) -> myApp.appendStrings2(a, b)));
// 引用特定类型的某一对象实例方法
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", String::concat));
//等价:
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (String a, String b) -> a.concat(b)));
particularTypeArbitraryObj(new Pika(), "Hello", Pika::testMethod); //T: Pika U: String R: int
// Pika::testMethod即 取第一个参数作为实例对象,第二个参数作为前者实例方法testMethod的参数
//等价于
particularTypeArbitraryObj(new Pika(), "Hello", (Pika a, String b) -> a.testMethod(b));
//可见,此类方法引用,本质是引用某对象的实例方法,只不过此时该对象作为第一个参数传入
}
}
class Pika {
public Pika() {
}
public int testMethod(String string) {
return 0;
}
}
方法引用 本质上,都是 lambda 表达式来 override 函数接口的 apply/accept 方法,比如,对于函数式接口 Comparator
int compare(T o1,T o2);
即 方法参数个数 2,参数类型 T, 返回值类型 int
上面提到的四种方法引用,最后都要转换为对应 lambda 表达式。
// 引用静态方法
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings));
//等价:
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (String a, String b) -> MethodReferencesExamples.appendStrings(a, b)));
// 引用某对象实例方法
MethodReferencesExamples myApp = new MethodReferencesExamples();
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", myApp::appendStrings2));
//等价:
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (String a, String b) -> myApp.appendStrings2(a, b)));
// 引用特定类型的某一对象实例方法
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", String::concat));
//等价:
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (String a, String b) -> a.concat(b)));
particularTypeArbitraryObj(new Pika(), "Hello", Pika::testMethod); //T: Pika U: String R: int
// Pika::testMethod即 取第一个参数作为实例对象,第二个参数作为前者实例方法testMethod的参数
//等价于
particularTypeArbitraryObj(new Pika(), "Hello", (Pika a, String b) -> a.testMethod(b));
//可见,此类方法引用,本质是引用某对象的实例方法,只不过此时该对象作为第一个参数传入
这里对该类引用做一个说明。
particularTypeArbitraryObj 方法中的函数接口参数定义如下:
BiFunction<T, U, R> funInterface
即参数个数 2,参数 1 类型为 T,参数 2 类型为 U,接口实现返回类型 R
准备引用的方法定义如下,注意testMethod()
不是静态方法,所以最终要通过引用方式 2,即某个对象的实例方法引用
class Pika {
public Pika() {
}
public int testMethod(String string) {
return 0;
}
}
方法引用的本质是还是最终的 lambda 表达式,先来对比以下 2 和 3 的 lambda 表达式:
//2 某个对象的实例方法引用
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (String a, String b) -> myApp.appendStrings2(a, b)));
//3 特定类型的某一对象实例方法引用
particularTypeArbitraryObj(new Pika(), "Hello", (Pika a, String b) -> a.testMethod(b));
可以观察到,函数接口实现 a.testMethod(b)
中,a 作为参数列表第一个参数,类型为 Pika;这里可以类比为 2 中的myApp
,3 中Pika a
作为参数列表第一个参数传入,myApp
是在其他地方实例化后传入的。
从是否符合函数接口定义的角度讲,两种引用方式的参数个数参数类型,包括实现里的返回值类型和个数,都符合函数接口定义。
总结,对于Pika::testMethod
特定类型
:方法引用的类Pika
作为 lambda 表达式参数列表第一个参数;引用方法testMethod
作为实例方法,返回值需要符合函数接口定义,并且接收后续参数(此处为 String b)作为方法入参。某一对象
: 从 lambda 表达式可以看出(Pika a, String b) -> a.testMethod(b)),第一个参数在实现中,作为实例对象,实现即调用实例对象的实例方法,并且将后续参数传给实例方法。所以某一对象即是形参列表第一个参数。这里有一个问题
particularTypeArbitraryObj(new Pika(), "Hello", Pika::testMethod);
这个方法引用属于第 3 类方法引用,是合法的,那么下面这种呢?
Pika myPika = new Pika();
particularTypeArbitraryObj(new Pika(), "Hello", myPika::testMethod);
讲道理,testMethod
作为实例方法,而非静态方法,调用实例对象的实例方法天经地义对吧?
别急,我们先看一下这种写法可能的 lambda 表达式:
particularTypeArbitraryObj(new Pika(), "Hello", (Pika a, String b) -> myPika.testMethod())
写到这里你应该明白了,对于 testMethod 方法的参数列表来说,只有一个 String 类型的参数,这时候参数列表第一个参数何去何从?
以下代码是合法的:
class Pika {
public Pika() {
}
public int testMethod(String string) {
return 0;
}
}
class Qiu {
public Qiu() {
}
public int testMethod(Pika pika, String string) {
return 0;
}
}
public static void main(String[] args) {
Qiu myQiu = new Qiu();
//引用特定类型的某一对象实例方法
particularTypeArbitraryObj(new Pika(), "Hello", myQiu::testMethod);
//等价于
particularTypeArbitraryObj(new Pika(), "Hello", (Pika a, String b) -> myQiu.testMethod(a, b));
}
这里的合法代码,可以看出来,属于第二类:引用特定类型的某一对象实例方法
注意myQiu::testMethod
, 只有第三类引用需要类和 lambda 参数列表第一个参数的类型保持一致,其他方法引用可以用其他类。
总结,只有第三类引用方法的参数列表个数(testMethod()的参数个数)比实际函数接口的参数列表接口少 1,其他情况,引用的方法参数个数总是符合函数接口定义。 少 1 的原因讲过了,lambda 参数列表第一个参数作为实例对象在后续函数实现中被调用了。
另外,lambda 表达式作为函数接口实现,要比方法引用适用范围宽泛得多。
例如,对于比较器:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
...
以下代码都是合法的:
public int compare1(Student student1, Student student2) {
return student1.getScore() - student2.getScore();
}
Comparator<Student> compare1 = (s1, s2) -> obj.compare1(s1, s2);
//甚至这种离谱的,都是合法的,结果和以上一致
public int compare2(Student student1, Student student2,Student student3) {
return student1.getScore() - student2.getScore();
}
Comparator<Student> compare2 = (s1, s2) -> obj.compare2(s1, s2, s2);
//compare2现在作为lambda具体实现的一部分,只需要保证返回值符合函数接口定义。
//只不过compare2的这种函数定义:参数个数3,参数类型Student 不再符合Comparator函数接口定义(参数个数2,参数类型T),所以不能作为引用方法了