1. 什么是方法引用?
我们已经知道如何编写Lambda表达式了,也就是说Lambda表达式提供了一种简约的范式给开发者,让开发者自定义处理逻辑。
但如果在开发当中,我们需要实现的Lambda表达式的逻辑,实际上其他对象中已经有现成的方法完全实现了这段处理逻辑,那这种情况下,我们是不是就不用再自定义Lambda表达式了,直接引用这个现成的方法来作为我们Lambda表达式的逻辑提供就好了。
这种直接引用其他方法逻辑作为Lambda表达式的方式,我们称为方法引用。
Lambda表达式是java8从语法层面提供的一种专门为函数式接口创建匿名实现类对象的一段逻辑表达式。
而方法引用同样是java8从语法层面简化Lambda表达式编写的一种语法升级,通过方法引用能够很方便的引用一个现成的方法来实现Lambda表达式的编写。
因此,方法引用是Lambda表达式的简化实现方式,也可以认为,方法引用是Lambda表达式的简写。
(1)方法引用并没有实际执行方法逻辑,而只是引用了方法的逻辑定义来作为Lambda表达式。
(2)方法引用实际上是Lambda表达式的另一种简化实现。
(3)方法引用转换为Lambda实际上依赖的是编译器的类型推断和参数推断,编译器会根据函数式接口的抽象方法定义中的参数类型和个数,自动找出符合条件的方法引用作为Lambda的逻辑实现。
2. 如何编写方法引用?
方法引用就是Lambda表达式的简写方式,因此它本质和Lambda表达式遵守的规则一样,即只能作为函数式接口的对象实现。
它们的规则都来自于要实现的那个函数式接口中的唯一那个抽象方法,即所有的参数和返回值等都从这个抽象方法中复制而来的。
方法引用中具体引用的是哪个方法,由函数式接口中抽象方法对应的形式参数和返回值决定。
正因如此:引用任何一个已经存在的方法时,必须确保该方法的参数个数、类型、顺序和返回值与要实现的函数式接口中的抽象方法中保持一致才可使用方法引用作为Lambda表达式。
还是那句话,方法引用是用来直接访问某个类或者某个实例对象中已经存在的方法或者构造方法。
方法引用提供了一种逻辑引用而不执行逻辑的方式,引用了某个方法,只是复制了这个方法体的逻辑,而并没有执行它,具体什么时候执行取决于该逻辑所表达的函数式对象何时触发它。
在真正执行计算时,方法引用会和Lambda表达式一样,创建一个函数式接口的实例对象,进而通过该实例对象去触发执行它。
简单来说,方法引用就是一个Lambda表达式,一个Lambda表达式在某些场景下可以被简写为方法引用,但注意,并不是所有的Lambda表达式都可以被简写。
方法引用本质上是一个更加紧凑、更加易读的Lambda表达式,它的操作符是双冒号 ::。
方法引用的标准编写语法是:
类型/实例对象名 :: 方法名称
其中,双冒号后面的方法名称,只需要写出方法名称即可,不需要()和任何参数,编译器将自动根据函数式接口的抽象方法参数和类型自动推断。
具体存在如下4种类型的方法引用:
(1)引用静态方法
ClassName::staticMethodName
(2)引用某个对象的实例方法
instanceName::instanceMethodName
(3)引用某个类的实例方法
ClassName::instanceMethodName
所有的方法引用中,就这个比较抽象,比较难理解。
按照我们过往的经验,通过类名只能调用静态方法;通过实例对象只能调用实例方法。
现在搞一个通过类名去调用实例方法,显得不伦不类。
这种方法引用实际上也是引用某个实例对象的实例方法的一种特殊情况下的变种。
什么情况下能使用这种方法引用呢?
记住:当需要引用的方法逻辑所需要的第一个参数就是当前调用该方法的实例对象时,才能使用这种方法引用。
(4)引用某个类的构造方法
ClassName::new
构造方法引用本质上也是静态方法引用,因为构造方法本身就是特殊的静态方法,只是它的实现通过new关键字去调用。
举个例子:
String::new
// 等价于Lambda表达式
()->new String()
当然这里面有数组构造方法的特殊情况需要注意,引用数组的构造方法形式如下:
TypeName[]::new
数组实际上也有构造方法的,只是它的构造方法比较特殊,比如 int[] a = new int[0];它的构造方法就是int[]类型。
要特别注意,数组构造器实际上是一个含有长度参数的构造方法,因此,int[]::new实际上是一个包含一个参数的构造器引用,这个参数就是数组的长度。
举个例子:
int[]::new
// 等价于Lambda表达式
(x)->new int[x];
// 上面的x就是int[]数组构造器中需要的长度参数
3. 方法引用案例
(1)编写一个函数式接口,作为Lambda表达式和方法引用操作的前提。
package zeh.myjavase.code42java8.demo03;
// 函数式接口
@FunctionalInterface
interface MethodReference {
// 只有一个抽象方法,没有提供实现。
// 接收一个int类型的参数a,没有任何返回值。
// 因此,Lambda表达式需要提供的实现逻辑必须符合要求:接收一个int类型的参数,不返回任何值。
void test(int a);
}
(2)接下来我们自定义一个类,用来提供Lambda所需要的现成方法引用。
package zeh.myjavase.code42java8.demo03;
// 自定义的方法引用提供者
public class CustomMethodRefProvider {
public CustomMethodRefProvider() {
}
// 提供一个参数为int的有参构造方法,作为函数式接口Lambda表达式的逻辑供应
public CustomMethodRefProvider(int a) {
System.out.println(a);
}
// 提供一个参数为int的静态方法,作为Lambda表达式的逻辑供应
public static void printInt(int a) {
int b = a + 1;
System.out.println(b);
}
// 提供一个参数为int的实例方法,作为Lambda表达式的逻辑供应
public void handleInt(int a) {
System.out.println(a);
}
}
(3)测试,分别使用自己编写Lambda表达式的方式,和直接使用方法引用作为Lambda表达式逻辑供应的方式来做验证。
package zeh.myjavase.code42java8.demo03;
import org.junit.Test;
public class MethodReferenceRun {
// 不使用方法引用,自己实现Lambda的逻辑编写。
// 输出 cehi123
@Test
public void testLambda() {
MethodReference testRef = (a) -> System.out.println("cehi" + a);
testRef.test(123);
}
// 引用PrintStream对象的println方法作为Lambda表达式的逻辑供应
// 输出 123
@Test
public void testMethodRef() {
MethodReference testRef = System.out::println;
testRef.test(123);
}
// 引用CustomMethodRefProvider的静态方法作为Lambda表达式的逻辑供应
// 输出 457
@Test
public void testMethodRef1() {
MethodReference testRef = CustomMethodRefProvider::printInt;
testRef.test(456);
}
// 引用CustomMethodRefProvider对象的实例方法作为Lambda表达式的逻辑供应
// 输出 789
@Test
public void testMethodRef2() {
MethodReference testRef = new CustomMethodRefProvider()::handleInt;
testRef.test(789);
}
// 引用CustomMethodRefProvider的构造方法作为Lambda表达式的逻辑供应
// 输出 110
@Test
public void testMethodRef3() {
MethodReference testRef = CustomMethodRefProvider::new;
testRef.test(110);
}
}
4. 借用一篇其他博主的方法引用介绍文章
原文链接
上一篇我们详细介绍了Optional类用来避免空指针问题,本篇我们全面了解一下Java8中的方法引用特性。
方法引用是lambda表达式的一种特殊形式,如果正好有某个方法满足一个lambda表达式的形式,那就可以将这个lambda表达式用方法引用的方式表示,但是如果这个lambda表达式的比较复杂就不能用方法引用进行替换。实际上方法引用是lambda表达式的一种语法糖。
在介绍方法引用使用方式之前,先将方法引用分下类
方法引用共分为四类:
1.类名::静态方法名
2.对象::实例方法名
3.类名::实例方法名
4.类名::new
4.1 首先来看下第一种 类名::静态方法名 为了演示我们自定义了一个Student类
public class Student {
private String name;
private int score;
public Student(){
}
public Student(String name,int score){
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public static int compareStudentByScore(Student student1,Student student2){
return student1.getScore() - student2.getScore();
}
public static int compareStudentByName(Student student1,Student student2){
return student1.getName().compareToIgnoreCase(student2.getName());
}
}
Student类有两个属性name和score并提供了初始化name和score的构造方法,并且在最下方提供了两个静态方法分别按score和name进行比较先后顺序。
接下来的需求是,按着分数由小到大排列并输出,在使用方法引用前,我们先使用lambda表达式的方式进行处理
Student student1 = new Student("zhangsan",60);
Student student2 = new Student("lisi",70);
Student student3 = new Student("wangwu",80);
Student student4 = new Student("zhaoliu",90);
List<Student> students = Arrays.asList(student1,student2,student3,student4);
students.sort((o1, o2) -> o1.getScore() - o2.getScore());
students.forEach(student -> System.out.println(student.getScore()));
sort方法接收一个Comparator函数式接口,接口中唯一的抽象方法compare接收两个参数返回一个int类型值,下方是Comparator接口定义
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
我们再看下Student类中定义的compareStudentByScore静态方法
public static int compareStudentByScore(Student student1,Student student2){
return student1.getScore() - student2.getScore();
}
同样是接收两个参数返回一个int类型值,而且是对Student对象的分数进行比较,所以我们这里就可以 使用类名::静态方法名 方法引用替换lambda表达式
students.sort(Student::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));
4.2 第二种 对象::实例方法名
我们再自定义一个用于比较Student元素的类
public class StudentComparator {
public int compareStudentByScore(Student student1,Student student2){
return student2.getScore() - student1.getScore();
}
}
StudentComparator中定义了一个非静态的,实例方法compareStudentByScore,同样该方法的定义满足Comparator接口的compare方法定义,所以这里可以直接使用 对象::实例方法名 的方式使用方法引用来替换lambda表达式
StudentComparator studentComparator = new StudentComparator();
students.sort(studentComparator::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));
4.3 第三种 类名::实例方法名
这种方法引用的方式较之前两种稍微有一些不好理解,因为无论是通过类名调用静态方法还是通过对象调用实例方法这都是符合Java的语法,使用起来也比较清晰明了。那我们带着这个疑问来了解一下这个比较特殊的方法引用。
现在再看一下Student类中静态方法的定义
public static int compareStudentByScore(Student student1,Student student2){
return student1.getScore() - student2.getScore();
}
虽然这个方法在语法上没有任何问题,可以作为一个工具正常使用,但是有没有觉得其在设计上是不合适的或者是错误的。这样的方法定义放在任何一个类中都可以正常使用,而不只是从属于Student这个类,那如果要定义一个只能从属于Student类的比较方法下面这个实例方法更合适一些.
public int compareByScore(Student student){
return this.getScore() - student.getScore();
}
接收一个Student对象和当前调用该方法的Student对象的分数进行比较即可。现在我们就可以使用 类名::实例方法名 这种方式的方法引用替换lambda表达式了.
students.sort(Student::compareByScore);
students.forEach(student -> System.out.println(student.getScore()));
这里非常奇怪,sort方法接收的lambda表达式不应该是两个参数么,为什么这个实例方法只有一个参数也满足了lambda表达式的定义(想想这个方法是谁来调用的)。这就是 类名::实例方法名 这种方法引用的特殊之处:当使用 类名::实例方法名 方法引用时,一定是lambda表达式所接收的第一个参数来调用实例方法,如果lambda表达式接收多个参数,其余的参数作为方法的参数传递进去。
结合本例来看,最初的lambda表达式是这样的
students.sort((o1, o2) -> o1.getScore() - o2.getScore());
那使用 类名::实例方法名 方法引用时,一定是o1来调用了compareByScore实例方法,并将o2作为参数传递进来进行比较。是不是就符合了compareByScore的方法定义。
4.4 类名::new
也称构造方法引用,和前两种类似只要符合lambda表达式的定义即可,回想下Supplier函数式接口的get方法,不接收参数有返回值,正好符合无参构造方法的定义
@FunctionalInterface
public interface Supplier<T> {
// Gets a result.
T get();
}
Supplier<Student> supplier = Student::new;
上面就是使用了Student类构造方法引用创建了supplier实例,以后通过supplier.get()就可以获取一个Student类型的对象,前提是Student类中存在无参构造方法。
小结:本篇全面介绍了方法引用的四种使用方式,且每种方式都有对应一个示例来帮助大家理解。当我们使用lambda表达式进行函数式编程时,如果某个方法正好满足lambda的定义,也满足实际需求的逻辑,就可以使用方法引用的方式来替换lambda表达式。接下来我们将真正开始学习stream api,并结合前面学习的内容体验stream api的强大之处。
5. 总结
再强调一下,没有函数式接口,Lambda表达式和方法引用就没法使用。
文档信息
- 本文作者:Marshall