在正式进入函数式编程之前,有必要先了解一下Java 8为支持函数式编程所做的基础性的改进,这里,将简要介绍一下FunctionalInterface注释、接口默认方法和方法句柄。
1、FunctionalInterface 注释
Java 8提出了函数式接口的概念。所谓函数式接口,简单来说,就是只定义了单一抽象方法的接口。比如下面的定义:
@FunctionalInterface
public interface IntHandler {
void handler(int i);
}
注释FunctionalInterface用于表明IntHandler接口是一个函数式接口,该接口被定义为只包含一个抽象方法handle(),因此它符合函数式接口的定义。如果一个函数满足函数式接口的定义,那么即使不标注为@FunctionalInterface,编译器依然会把它看做函数式接口。这有点像@Override注释,如果你的函数符合重载的要求,无论你是否标注了@Override,编译器都会识别这个重载函数,但一旦你进行了标注,而实际的代码不符合规范,那么就会得到一个编译错误。如图6.1所示,展示了一个不符合规范,却被标注为@FunctionalInterfacede接口。很显然,该IntHandler包含两个抽象方法,因此不符合函数式接口的要求,又因为IntHandler接口被标注为函数式接口,产生矛盾,故编译出错。
不符合规范的函数式接口
这里需要强调的是,函数式接口只能有一个抽象方法,而不是只能有一个方法。这分两点来说明:首先,在Java 8中,接口运行存在实例方法,其次任何被java.lang.Object实现的方法,都不能视为抽象方法,因此,下面的NonFunc接口不是函数式接口,因为equals()方法在java.lang.Object中已经实现。
public interface NoFunc {
@Override
boolean equals(Object object);
}
同理,下面实现的IntHandler接口符合函数式接口要求,虽然看起来它不像,但实际上它是一个完全符合规范的函数式接口。
@FunctionalInterface
public interface IntHandler {
void handler(int i);
@Override
boolean equals(Object object);
}
2、接口默认方法
在Java 8之前的版本,接口只能包含抽象方法。但从Java 8之后,接口也可以包含若干个实例方法。这一改进使得Java 8拥有了类似于多继承的能力。一个对象实例,将拥有来自于多个不同接口的实例方法。 比如:对于接口IHorse
public interface IHorse{
void eat();
default void run(){
System.out.println();
}
}
在Java 8中,使用default关键字,可以在接口内定义实例方法。注意,这个方法并非抽象方法,而是拥有特定逻辑的具体实例方法。 所有的动物都能自由呼吸,所以,这里可以再定义一个IAnimal接口,它也包含一个默认方法breath()。
public interface IAnimal {
default void breath(){
System.out.println("breath");
}
}
骡是马和驴的杂交物种,因此骡(Mule)可以实现为IHorse,同时骡也是动物,因此有:
public class Mule implements IHorse,IAnimal{
@Override
public void eat() {
System.out.println("Mule eat");
}
public static void main(String[] args) {
Mule m=new Mule();
m.run();
m.breath();
}
}
注意上述代码中Mule实例同时拥有来自不同接口的实现方法。这在Java 8之前是做不到的。从某种程度上说,这种模式可以弥补Java单一继承的一些不便。但同时也要知道,它也将遇到和多继承相同的问题。
如果IDonkey也存在一个默认的run()方法,那么同时实现它们的Mule,就会不知所措,因为它不知道应该以哪个方法为准。
public interface IDonkey{
void eat();
default void run(){
System.out.println("Donkey run");
}
}
graph TD
Start --> Stop
修改骡Mule的实现如下,注意它同时实现了IHorse和IDonkey:
public class Mule implements IHorse,IAnimal,IDonkey{
@Override
public void eat() {
System.out.println("Mule eat");
}
public static void main(String[] args) {
Mule m=new Mule();
m.run();
m.breath();
}
}
// 此时,由于IHorse和IDonkey拥有相同的默认实例方法,故编译器会抛出一个错误:
Duplicate default methods named run with the parameters () and () are inherited from the types IDonkey and IHorse
为了让Mule同时实现IHorse和IDonkey,在这里,我们不得不重新实现一下run()方法,让编译器可以进行方法绑定。修改Mule的实现如下:
public class Mule implements IHorse,IDonkey,IAnimal{
@Override
public void run(){
IHorse.super.run();
}
@Override
public void eat() {
System.out.println("Mule eat");
}
public static void main(String[] args) {
Mule m=new Mule();
m.run();
m.breath();
}
}
```mermaid
interface IHorse {
+eat()
#run()
}
interface IDonkey {
+eat()
#run()
}
interface IAnimal {
+breath()
}
class Mule{
+eat()
#run()
+breath()
}
IHorse <|-- Mule
IAnimal <|-- Mule
IDonkey <|-- Mule
在这里,将Mule的run()方法委托给IHorse实现,当然,大家也可以有自己的实现。
接口默认实现对于整个函数式编程的流式表达非常重要。比如,大家熟悉的java.util.Comparator接口,它在JDK 1.2时就已经被引入,用于在排序时给出两个对象实例的具体比较逻辑。在Java 8中,Comparator接口新增了若干个默认方法,用于多个比较器的整合。其中一个常用的默认方法如下:
```java
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
有了这个默认方法,在进行排序时,我们就可以非常方便地进行元素的多条件排序,比如,如下代码构造一个比较器,它先按照字符串长度排序,继而按照大小写不敏感的字母顺序排序。
Comparator<String> cmp = Comparator.comparingInt(String::length).thenComparing(String.CASE_INSENSITIVE_ORDER);