为什么要有默认方法
在 Java8 之前,接口与其实现类之间的耦合度太高了,当需要为一个接口添加方法时,所有的实现类都必须随之修改。默认方法解决了这个问题,它可以为接口添加新的方法,而不会破坏已有的接口的实现。这在 lambda 表达式作为 Java8 语言的重要特性而出现之际,为升级旧接口且保持向后兼容提供了途径。
1
2
3
|
String[] array = new String[] { "hello", ", ", "world", };
List<String> list = Arrays.asList(array);
list.forEach(System.out::println); // 这是 jdk 1.8 新增的接口默认方法
|
这个forEach
方法是 jdk 1.8 新增的接口默认方法,正是因为有了默认方法的引入,才不会因为Iterable
接口中添加了forEach
方法就需要修改所有Iterable
接口的实现类。
1
2
3
4
5
6
7
8
|
public interface Iterable<T> {
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
|
默认方法的继承
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
|
interface InterfaceA {
default void foo() {
System.out.println("InterfaceA foo");
}
}
interface InterfaceB extends InterfaceA {
}
interface InterfaceC extends InterfaceA {
@Override
default void foo() {
System.out.println("InterfaceC foo");
}
}
interface InterfaceD extends InterfaceA {
@Override
void foo();
}
public class Test {
public static void main(String[] args) {
new InterfaceB() {}.foo(); // 打印:“InterfaceA foo”
new InterfaceC() {}.foo(); // 打印:“InterfaceC foo”
new InterfaceD() {
@Override
public void foo() {
System.out.println("InterfaceD foo");
}
}.foo(); // 打印:“InterfaceD foo”
// 或者使用 lambda 表达式
((InterfaceD) () -> System.out.println("InterfaceD foo")).foo();
}
}
|
接口默认方法的继承分三种情况(分别对应上面的 InterfaceB 接口、InterfaceC 接口和 InterfaceD 接口):
- 不覆写默认方法,直接从父接口中获取方法的默认实现。
- 覆写默认方法,这跟类与类之间的覆写规则相类似。
- 覆写默认方法并将它重新声明为抽象方法,这样新接口的子类必须再次覆写并实现这个抽象方法。
默认方法的多继承
Java 使用的是单继承、多实现的机制,为的是避免多继承带来的调用歧义的问题。当接口的子类同时拥有具有相同签名的方法时,就需要考虑一种解决冲突的方案。
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
|
public class DefaultMethodMuiltExtendTest {
interface InterfaceA {
default void foo() {
System.out.println("InterfaceA foo");
}
}
interface InterfaceB {
default void bar() {
System.out.println("InterfaceB bar");
}
}
interface InterfaceC {
default void foo() {
System.out.println("InterfaceC foo");
}
default void bar() {
System.out.println("InterfaceC bar");
}
}
class ClassA implements InterfaceA, InterfaceB {
}
// 错误
// class ClassB implements InterfaceB, InterfaceC {
// }
class ClassB implements InterfaceB, InterfaceC {
@Override
public void bar() {
InterfaceB.super.bar(); // 调用 InterfaceB 的 bar 方法
InterfaceC.super.bar(); // 调用 InterfaceC 的 bar 方法
System.out.println("ClassB bar"); // 做其他的事
}
}
}
|
在 ClassA 类中,它实现的 InterfaceA 接口和 InterfaceB 接口中的方法不存在歧义,可以直接多实现。
在 ClassB 类中,它实现的 InterfaceB 接口和 InterfaceC 接口中都存在相同签名的 foo 方法,需要手动解决冲突。覆写存在歧义的方法,并可以使用InterfaceName.super.methodName();
的方式手动调用需要的接口默认方法。
接口继承行为发生冲突时的解决规则
有如下情况
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
|
interface InterfaceA {
default void foo() {
System.out.println("InterfaceA foo");
}
}
interface InterfaceB extends InterfaceA {
@Override
default void foo() {
System.out.println("InterfaceB foo");
}
}
// 正确
class ClassA implements InterfaceA, InterfaceB {
}
class ClassB implements InterfaceA, InterfaceB {
@Override
public void foo() {
// InterfaceA.super.foo(); // 错误
InterfaceB.super.foo();
}
}
|
当 ClassA 类多实现 InterfaceA 接口和 InterfaceB 接口时,不会出现方法名歧义的错误。当 ClassB 类覆写 foo 方法时,无法通过 InterfaceA.super.foo(); 调用 InterfaceA 接口的 foo方法。
因为 InterfaceB 接口继承了 InterfaceA 接口,那么 InterfaceB 接口一定包含了所有 InterfaceA 接口中的字段方法,因此一个同时实现了 InterfaceA 接口和 InterfaceB 接口的类与一个只实现了 InterfaceB 接口的类完全等价。
这很好理解,就相当于 class SimpleDateFormat extends DateFormat 与 class SimpleDateFormat extends DateFormat, Object 等价(如果允许多继承)。
而覆写意味着对父类方法的屏蔽,这也是 Override 的设计意图之一。因此在实现了 InterfaceB 接口的类中无法访问已被覆写的 InterfaceA 接口中的 foo 方法。
建议
通过这个例子,应该注意到在使用一个默认方法前,一定要考虑它是否真的需要。因为 默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。滥用默认方法可能给代码带来意想不到、莫名其妙的错误。