默认方法

默认方法

一. 引文

Java程序的接口是将相关方法按照约定组合到一起的方式,实现接口的类必须为接口中定义的每一个方法提供实现,或者从父类继承实现。这样如果设计者需要更新接口,向其中加入新的方法,就会引起一些问题,因为大部分实现类并不由设计者所管控,但实现类必须为了适配接口的更新而修改。

你可以选择为API创建不同的发布版本,同时维护老版本和新版本。但这种做法增加了作为类库设计者维护类库的复杂度,类库的用户也不得不同时使用一套代码的两个版本,且这会增加内存的消耗,延长程序的载入时间,因为这种方式下项目使用的类文件数量更多了。

为了解决上述问题,Java 8支持在接口声明方法的同时提供实现。第一,接口内可以声明静态方法;第二,引入新功能——默认方法,可以指定接口方法的默认实现。默认方法的设计初衷是为了支持库设计师,为了以更兼容的方式解决像Java API这样的类库的演进问题,辅助他们写出更容易改进的接口。

向接口添加方法

比如在Java 8之前,List是没有stream()或parallelStream()方法的,List所继承的Collection接口也没有。那么对于设计者而言,最简单的做法就是把stream()方法加入Collection接口,并加入ArrayList类的实现。但这样的更新使Collection接口多出一个方法,以前版本中会有大量依照Collection接口而扩展的实体类,他们就必须都实现stream()方法,那么怎样才能改变已发布的接口而不用破坏已有的实现呢

1
2
3
List<Student> students1 = inventory.stream().filter((Student s) -> s.getAge() > 18).collect(toList());

List<Student> students2 = inventory.parallelStream().filter((Student s) -> s.getAge() > 18).collect(toList());

Java 8采用的做法就是允许接口包含实现类没有提供实现的方法签名,缺失的这些方法由接口来完成实现。所以Java 8提供了default关键字来在接口中实现默认方法

1
2
3
4
5
6
7
8
9
10
//List源码中sort方法的默认实现
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}

类可以实现多个接口,当默认实现冲突时,Java有一套处理和限制来避免类似C++的菱形继承问题。

抽象类和抽象接口的区别?

  • 一个类只能继承一个抽象类,但一个类可以实现多个接口
  • 一个抽象类可以通过实例变量保存一个通用状态,而接口不能有实例变量

二. 使用默认方法

2.1 可选方法

类实现了接口,但会刻意的将一些方法的实现留白,如Iterator接口的remove方法,Java 8以前因为用户很少使用,所以remove常被忽略,实现Iterator接口的类通常会为此方法放置一个空的实现。

采用默认方法后,刻意为这种类型的方法提供一个默认实现,实体类就无需再重复的提供一个空方法。

1
2
3
default void remove() {
throw new UnsupportedOperationException("remove");
}

2.2 行为的多继承

行为的多继承,一种让类从多个来源重用代码的能力。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
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
/**
* 可旋转
*/
public interface Rotatable {
void setRotationAngle(int angleInDegress);
int getRotationAngle();

default void rotateBy(int angleInDegress){
setRotationAngle((getRotationAngle() + angleInDegress) % 360);
}
}

/**
* 可移动
*/
public interface Moveable {
int getX();
int getY();
void setX(int x);
void setY(int y);

default void moveHorizontally(int distance){
setX(getX() + distance);
}

default void moveVertically(int distance){
setY(getY() + distance);
}
}

/**
* 可调整大小
*/
public interface Resizeable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);

default void setRelativeSize(int wFactor, int hFactor){
setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}
}

/**
* 可移动、旋转和缩放
*/
public class Monster implements Rotatable,Moveable,Resizeable{
......
}

/**
* 可移动和旋转
*/
public class Sun implements Moveable,Rotatable {
......
}

public class Test {
public static void main(String[] args) {
Monster m = new Monster();
m.rotateBy(180);
m.moveVertically(10);
Sun sun = new Sun();
sun.moveHorizontally(100);
sun.rotateBy(6);
}
}

多种行为的组合

通过组合接口Rotatable、Moveable、Resizeable可以创建不同的实现类,需要实现定义的抽象方法,无需重复实现默认方法。

继承不是适合所有代码复用场景的万能钥匙,比如一个复杂类的继承就不是好的选择,而是应该通过代理,即创建一个方法通过该类的成员变量直接调用该类的方法。这也是为什么有时我们会声明一个类的类型为final:声明为final的类无法被其他类继承,避免发生这样的反模式,防止核心代码的功能被污染。这样的思想也适用于使用默认方法的接口,我们只需选择需要的实现即可。


三. 解决冲突的规则

一个类继承了多个使用同样函数签名的方法,如何选择使用的函数?如下列代码,类C继承了接口A和B的同名方法,最终会选择哪个?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface A {
default void hello(){
System.out.println("Hello from A");
}
}

public interface B extends A {
default void hello(){
System.out.println("Hello from B");
}
}

public class C implements A,B{
public static void main(String[] args) {
new C().hello();//Hello from B
}
}

3.1 解决冲突的三个规则

如果一个类使用相同的函数签名从多个地方继承了方法,通过三条规则可以进行判断。

如何判断相同签名方法选择优先级?

  1. 类中的方法优先级最高。类或父类中声明的方法优先级高于任何声明为默认方法的优先级。
  2. 如果无法依据第一条进行判断,则子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,则B比A更具体。
  3. 最后还是无法判断,则继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式的选择使用哪一个默认方法的实现。

所以对于上述代码案例,同样是默认方法,会优先选择接口B中的方法。

3.2 冲突及如何显式的消除歧义

如果前两条规则都无法判断优先级时,Java编译器会抛出一个编译错误,因为编译器无法判断应该选择哪个方法。解决这种冲突没有太多的方案,只能显式的决定希望使用哪一种方法,所以可以在类C中覆盖hello方法,在它的方法体内显式的调用希望调用的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface A {
default void hello(){
System.out.println("Hello from A");
}
}

public interface BB {
default void hello(){
System.out.println("Hello from BB");
}
}

public class E implements A,BB{
public void hello() {
BB.super.hello();
}
public static void main(String[] args) {
new E().hello();//Hello from BB
}
}

Java 8引入了一种新的语法:X.super.m(…),X是希望调用的m方法所在的父接口。

3.3 菱形继承问题

如下述代码所描绘情况,因为类的继承关系图形像菱形所以被称作菱形问题。下列情况最终选择的必然是接口A的hello方法,如果B接口也实现了hello方法,则因为B更具体所以会选择B的hello方法。如果C也实现了hello方法,就出现了冲突,需要显式的指定要使用的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface A {
default void hello(){
System.out.println("Hello from A");
}
}
public interface B extends A {
}
public interface C extends A {
}
public class D implements B, C {
public static void main(String[] args) {
new D().hello();//Hello from A
}
}

菱形问题


参考:

🔗 《Java8实战》