学习Java函数式编程(二):Java 8接口新特性

本文主要介绍Java 8引入的默认方法以及接口静态方法等新特性,涉及了Lambda表达式作为参数参与方法重载、引入了默认方法的接口多重继承以及Optional数据类型等内容。

默认方法

简单的说,默认方法的出现是为了保持Java语言不同版本的兼容性。比如Collection接口增加了stream方法,每个实现了Collection接口的子类都要添加对方法stream的实现,如果只是JDK内部还好,但非JDK的类也可能实现了之前版本的Collection,这样在没有重写的情况下必然造成不兼容。

例如Iterable接口就添加了新的默认方法:forEach,和for的区别是可以传入Lambda表达式作为循环体:

1
2
Iterable<Integer> iterator = list ; // 1, 2, 3, 4, 5
iterator.forEach(x -> System.out.println(x * x));

它的内部实现是这样的:

1
2
3
4
5
default void forEach(Consumer<? super T> action) {
for (T t : this) {
action.accept(t);
}
}

切记它的返回值是void。

在Java 8中如果涉及复杂的继承和实现关系,调用的到底是哪一个类或者接口的方法呢?

《Java 8函数式编程》中采用了如下的例子进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class TestExtendsDefaultMethodDemo {
public static void main(String[] args) {
// 子类ParentImpl实现了接口Parent,但是没有重写接口的默认方法
Parent parentimpl = new ParentImpl();
parentimpl.welcome();
System.out.println(parentimpl.getMessage());
// 另一个接口Child继承了接口Parent并重写了其默认方法,构造子类ChildImpl实现接口Child
Child childimpl = new ChildImpl();
childimpl.welcome();
System.out.println(childimpl.getMessage());
// 用一个新类OverridingParent继承ParentImpl,并重写默认方法
Parent overridingParent = new OverridingParent();
overridingParent.welcome();
System.out.println(overridingParent.getMessage());
// 创建新的类OverridingChild继承OverridingParent并实现Child
Child child = new OverridingChild();
child.welcome();
System.out.println(child.getMessage());
}
static interface Parent {
public void message(String body);
public default void welcome() {
message("Parent");
}
public String getMessage();
}
static class ParentImpl implements Parent {
String body;
@Override
public void message(String body) {
this.body = body;
}
@Override
public String getMessage() {
return body;
}
}
static interface Child extends Parent {
@Override
default void welcome() {
message("Child");
}
}
static class ChildImpl implements Child {
String body;
@Override
public void message(String body) {
this.body = body;
}
@Override
public String getMessage() {
return body;
}
}
static class OverridingParent extends ParentImpl {
@Override
public void welcome() {
message("OverridingParent");
}
}
static class OverridingChild extends OverridingParent implements Child {}
}

该代码输出为:

1
2
3
4
5
6
Parent
Child
OverridingParent
OverridingParent
Process finished with exit code 0

到底调用谁的方法,可以从虚拟机运行时的层面进行判断,如果是动态分派,执行编译出来invokevirtual指令之前,先将当前调用方法的实例的引用this压入所处栈帧的操作数栈,接着是参数列表的参数(最后这些数据都会按顺序拷贝一份到确认了调用的方法的局部变量表中)。invokevirtual指令会找到this所对应的类,不妨设为C,在C中找到描述符和简单名都相符的方法,找不到就按照继承关系从下往上查找,先子类再父类。

默认方法的出现,衍生出了接口多重继承的问题,因为现在可以出现两个接口的方法签名一样但是实现不一样的情况了,

当javac不能明确继承的是哪个接口的方法的时候就会报错,解决的方法有两种:

  • 直接重写该默认方法;
  • 使用增强的super来指定调用的是哪个接口的默认方法;

下面给出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface A {
default void test01() {
System.out.println("A");
}
}
interface B {
default void test01() {
System.out.println("B");
}
}
class C implements A, B {
@Override
public void test01() {
// System.out.println("C");
A.super.test01();
}
}

接口静态方法 & Optional

接口静态方法的出现更像是对Java编程的一种规范,本人之前在写普通类的时候常常贯穿各种静态方法,现在提倡将静态方法统一放在一个接口当中作为工具方法。

Optional的出现是为了应对空指针问题,空指针有的时候就像幽灵,本人以前写过一个小项目,一个空指针问题一直埋在代码中很长时间未被发现,因为null在某些时候并不会造成程序的崩坏。关于Optional本站后续还将进行探讨。

参考

《Java 8函数式编程》