方法引用:
之前花了很多时间对Lambda表达式进行了深入的学习,接下来开启新的主题---方法引用(Method References),其实在之前的学习中已经使用过了,如:
那方法引用跟Lambda表达式是一种什么关系呢?其实可以理解为它是Lambda表达式的一个语法糖(Syntactic sugar),什么是语法糖呢,可以看一下百科:
在很多时候在能使用Lambda表达式实现的功能,恰巧该功能刚好有一个方法能表达出来,那么就可以通过方法引用的方式来替换掉Lambda表达式使得代码更加精简,更加漂亮。当然方法引用是有局限性的,并非是一种非常通用的结构,为啥,因为很多时候Lambda表达式并非能用方法引用来替换,那换句话:什么时候Lambda表达式能用方法引用替换呢?主要看Lambda表达式的方法体里面的实现,如果恰好方法体里面的实现刚好有一个方法能提供该功能那就可以采用方法引用的方式替换,相反如果方法体里面的功能比较复杂并未有现成的方法来提供那就不能替换。
空谈了这么多,没有代码的体现当然跟听天书一样,下面开始撸码,先从集合元素的打印开启方法引用的探索:
没啥可解释的,接着可以变换方法引用的样式,如下:
很显然使用过方法引用的方式代码更加简洁,当然乍一看也是更加难以读懂滴,当然系统掌握了方法引用了之后就个难懂就会变为习以为常滴啦,接着继续,其中当鼠标点击"::"时,IDE会自动跳到函数式接口里,这里就不演示了,也就是确实是Lamdba表达式的另一种表现形式,接着来仔细观察一下这种方法引用的写法:
System.out是代表什么呢,点进去看一下:
而"::"的右侧的println,则是PrintStream类中的println()实例方法:
这是方法引用的一种表现形式,当然之后会详细介绍方法引用的所有方式,这是后话,这里先来针对这个例子讨论一下方法引用,其实我们可以将方法引用看作是一个函数指针【function pointer】,函数指针不是c、c++的概念么,当然这里是可以理解成函数指针啦,怎么理解:
接下来由这个示例做为引子来对方法引用做一个详细的介绍,在上面也说了方法引用是有多种表现形式的,其实方法引用是有4类,下面则一一进行详细学习。
1、类名::静态方法名
这里就直接上代码进行学习,以对学生进行排序为场景,先新建学生的实体:
因为要根据名字与成绩进行排序,所以将其排序的方法直接定义在Student实体中,而又因为当前探讨的方法引用形式是"类名::静态方法名",所以将这两个排序方法也定义为static的【先不用管这么写代码的合理性,主要是为了方便说明问题】:
public class Student { private String name; private int score; 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 comparedStudentByScore(Student student1, Student student2) { return student1.getScore() - student2.getScore(); } /** * 根据学生名字的ASCII码进行升序排序 */ public static int comparedStudentByName(Student student1, Student student2) { return student1.getName().compareToIgnoreCase(student2.getName()); } }
接着初始化一个集合:
然后对集合元素进行排序,传统的作法是:
但是在Java1.8中List中直接增加了排序的方法如下:
查看一下该方法的源码:
所以这里用Lambda表达式用这种方法来排序,如下:
上面这种写法已经比较熟了,接着引入主题,采用方法引用的方式来进行排序,在写代码之前先观察一下这个排序:
满足这个特性的代码刚好就可以用方法引用来改写,如下:
很显然第一种方法引用的方式就出现了:
细细体会一下:
所以进一步说明方法引用就是Lambda表达式的一种语法糖,功能一样,只是代码看起来更加简洁了,其实对于方法引用的使用是有一个要求的:你所使用的Lambda表达式的方法体恰好有一个匹配的方法完成相同的功能,这时候就可以将Lambda表达式替换成方法引用,除此之外是无法用方法引用替换Lambda表达式的,所以说Lambda表达式是一种更为通用的方式,而方法引用是需要满足一定的条件才能够使用的。
接着根据用户名来进行排序,so easy啦:
【注意】:这上"::"这是在Java8中新增的一种语法形式,它的左边是什么和右边是什么方法都得取决于上下文。
2、引用名【对象名】::实例方法名
这里为了说明这种类型,新建一个类,然后将比较器由之前的静态方法声明为实例方法:
下面使用这个新的比较器对象来同样对元素进行排序,还是先用Lambda表达式来实现:
接着使用新的方法引用的形式来替换:
同样的如果想根据名字来进行排序:
3、类名::实例方法名【最难理解】
如小标题所示,这种方法引用的方式是所有方式中最维理解的,为什么呢?通常如果用类名去调用方法那这方法必然是静态的,然而这里居然用类名直接可以跟上实例方法名,这不逆天了么?其实产生这种疑惑的主要根源在于将方法引用联想成了方法调用,这两个是完全不一样的概念,所以不要混为一谈啦。另外这种方式跟上面的两种方式在参数的传递上是有一些不同的, 下面通过例子来看一下:
先来看一下咱们在Student中定义的两个静态比较方法:
那接下来修改代码来弥补这种设计上的缺失:
接下来调用一下新设计的方法:
接下来理解一下这种写法,有些奇怪,sort()方法接收的是一个ComParator接口,而它是需要接收两个参数的,而看一下咱们目前传递的:
那对应不上了呀,其实理解这种写法核心在于:
这个方法一定是由sort()方法接受的Lambda表达式第一个参数来调用的,而如果Lambda表达式有多个参数,则除了第一个参数之外的所有参数都将作为方法的参数传递进去,还是有些抽象,这里用代码进一步理解:
哇~~好隐晦的,所以说这种方法引用是最难理解的,不过理解了之后再看到这样的写法就比较容易懂了,那接下来对名字再进行排序雷同的写法啦:
接下来再来举个新的例子,对城市列表名进行排序,先用传统的方式,代码如:
接着改用方法引用的方式,由于String中的compareToIgnoreCase()方法正好符合第三种方法引用的要求,如下:
所以说改用方法引用的写法如下:
当然对于打印这块的语句也能改成方法引用,如下:
先看一下out是什么东东:
而println()是PrintStream中的实例方法:
所以这是咱们学的第二种方法引用:对象名::实例方法名。
4、构造方法引用,其表现形式为:类名::new
这是最后一个方法引用的形式,学完那方法引用就可以搞通啦,加油~~下面看代码:
其Supplier的函数原型这里回顾一下:
接着来调用一下它,先想一下,对于String的构造方法是不是刚好满足Supplier的函数要求:不接收参数返回一个String,所以可以直接用方法引用的方式:
编译运行:
再回过头来用鼠标点一下方法引用会发现:
可见IDE直接就识别了是调用了String的无参的构造方法。这就是构造方法引用的使用方式,接下来继续:
接着调用一下它:
依然可以用这种引用方式,为什么,这时点鼠标看IDE识别到了哪个方法:
很显然定位到了带参数的String构造函数去了,而它正好符合Function函数的要求:接收一个参数,返回一个参数。
以上就是涉及到方法引用的所有形式,需要好好消化。
默认方法【default method】:
关于默认方法在之前的学习中也反复提及到了,这里再集中学习一下它,默认方法的定义应该不用过多解释了,就是在接口中有具体实现的方法必须要带上default关键字,而这样的方法就称之为默认方法,下面咱们自己定义一个接口:
然后再新建一个类直接实现该接口:
比较好理解,接着再定义一个接口:
那问题来了,如果MyClass同时实现这两个接口会有什么情况?最终打印的myMethod会是哪个接口的呢?下面试试就知道了:
对于这个错误其实也能理解:因为myMethod同时存在于MyInterface1、MyInterface2接口当中,那对于MyClass而言当然不知道要继承哪一个接口的方法,所以这时要解决这个错误就需要在MyClass中重写myMethod方法,如下:
那如果就想执行它继承的某个接口的myMethod()方法呢?可以这样处理:
接下来再来折腾,新建一个实现类来实现MyInterface1,如下:
接下来让MyClass类继承它,并实现MyInterface2,如下:
可见编译器没有报错,那请问这时打印的结果是?
为什么?这其实是Java的一个约定:实现类的优先级要比接口的优先级要更高一些,如下: