函数式接口
在Java8之前,想做到传递一个函数或者一个行为非常的不容易。为了做到回调这种效果,以前的做法是创建并传入一个匿名内部类变量,写一大段与行为无关的代码,非常的繁琐。
list.sort(new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return 0;
}
})
而在Java8,有了函数式接口之后,可以大大的简化代码的编写,达到一样的效果。
list.sort((o1, o2) -> o1 - o2);
为什么可以达到这种效果呢?
正是由于 Comparator这个接口在Java8之后,就成为了一个函数式接口,而lambda表达式又是实现函数式接口的其中一种方式。
先回顾一下函数式接口javadoc的几点事项:
一个接口拥有且仅有一个抽象方法。
如果接口中重写了Object类中的抽象方法,这些抽象方法不参与第一点的判断。
可以使用lamdba表达式、方法引用、构造方法引用来创建函数式接口的一个实例。
有了第三点,显然我们可以使用lamdba表达式来创建函数式接口的一个实例,从而代替了以前声明一个匿名内部类变量的做法。
有了函数式接口之后,用来创建这个接口实例的lambda表达式肯定不能乱写呀,它的入参和返回类型在冥冥之中显然受到了某种约束! 这种约束其实就在于函数式接口那唯一的抽象方法之中。
int compare(T o1, T o2);
可以看到,Comparator函数式接口中的抽象方法compare中需要两个入参和一个int类型的返回类型。那么我们创建接口实例的方式就可以写为lambda表达式:
(T o1, T o2) -> (int) ...
这样,我们就实现了compare这个抽象方法,至于在List.sort这个方法中如何调用compare这个方法,已经脱离函数式接口这个范畴了,所以这部分的实现在Java7或Java8都是一样的。关键是搞清楚lambda表达式在函数式接口中做到了什么事情、起到了什么作用。
当然了,lambda表达式只是其中的一种方式,除了lambda表达式之外,还有方法引用和构造方法引用。
高阶函数
如果一个方法,接收一个函数式接口作为参数、或者是返回一个函数式接口,那么这个方法就称为高阶函数。
举个例子,在Iterable接口中有一个默认方法叫forEach,它接收一个Consumer的函数式接口,返回void,因此forEach成为高阶函数。
default void forEach(Consumer super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
在forEach这个方法中,通过iterator遍历容器,并对每个元素执行Consumer接口的抽象方法 accept。
public interface Consumer {
void accept(T t);
}
因此,如果我们要做到为容器每个元素执行某种操作的效果,就可以使用forEach这个方法,并传入一个Consumer函数式接口的一个实现,即一段行为的具体操作。
方法引用
方法引用可以看作一种特殊的lambda表达式,如果你要描述的行为恰好有现成的一个方法时,可以直接使用该方法的方法引用,达到创建函数式接口实例的作用。
需要注意的是,方法引用与方法调用是完全两回事,它指的是一个函数指针,而不是具体的一个调用。本质上,还是用来描述如何创建一个函数式接口的实例。
回顾下方法引用,主要分为四种。
类型
示例
引用静态方法
className::staticMethodName
引用对象的实例方法
instanceName::instanceMethodName
引用类的实例方法
className::instanceMethodName
引用构造方法
className::new
在使用方法引用的时候,Java会根据上下文来断定该方法引用映射到哪一个函数式接口上。
常见的两种断定情景如下:
// 『 :: 』 这个符号对应了声明类型Consumer
Consumer consumer = System.out::println;
// 『 :: 』 这个符号对应了forEach方法的参数类型Consumer
arr.forEach(System.out::println);
当『方法引用』 用来赋值接口的时候,它的入参个数、类型和返回类型需要与所声明函数式接口的抽象方法相对应。
当『方法引用』 作为参数传入高阶函数时,它的入参个数、类型和返回类型需要与高阶函数的函数式接口参数的抽象方法相对应。
除了『引用类的实例方法』之外,其他类型的方法引用中,它引用的方法的参数与函数式接口的抽象方法参数的个数和顺序保持相同。且它的参数类型和返回类型必须为函数式接口抽象方法的同类或者是『父类』。
例如,由于函数式接口Consumer的抽象方法accept 接收一个参数,不返回值,而System.out的println方法也是接收一个参数,不返回值,因此我们可以使用println这个方法引用来赋值给Consumer,或者是作为Consumer类型的参数进行传递。
需要注意的是,如果方法引用使用的是『引用类的实例方法』,它会把实例方法的调用对象作为函数式接口抽象方法的第一个参数。
例如,String的实例方法toUpperCase方法是个无入参的实例方法,但它作为『引用类的实例方法』的方法引用时,可以赋值给『一个入参』的Function函数式接口,却无法赋值给『无入参』的Supplier接口。这是由于toUpperCase方法作为『引用类的实例方法』的方法引用时,它会将调用者对象作为Function的第一个参数。
public String toUpperCase() {
return (String)…… ;
}
// 使用『引用对象的实例方法』
Supplier sup = "instance"::toUpperCase;
// 使用『引用类的实例方法』
Function fun = String::toUpperCase;
可以看出,对于同一个方法,不同的引用方式,所创建的函数式接口可能是不同的。
构造方法引用
构造方法引用是方法引用的其中一种,由于构造方法没有方法名,所以拼写方法引用的时候使用『new』作为第三个部分,比如,
Supplier supInstance = String::new;
Function funInstance = String::new;
Function funCharInstance = String::new;
同样地,Java根据上下文来断定方法引用所创建的是哪一个函数式接口实例。
当使用Supplier声明变量时,该『方法引用』引用的是『无参数』的构造方法。
当使用Function声明变量时,该『方法引用』引用的是『一个参数』的构造方法。
此外,Function接口声明的参数泛型使得『方法引用』指向具体的重载方法。