第21条:为后代设计接口
一个接口一旦公布就几乎不可更改,因此,在设计接口时,必须具有前瞻性,充分考虑其未来的演变,否则微小的缺陷也可能导致巨大的代价。
在web开发其实还好,基本一个接口就只有一个实现类,添加新接口直接实现就好了。
接口的脆弱性
在Java 8之前,向已发布的接口添加新方法会“破坏”所有现有的实现类,因为它们没有实现新方法。Java 8引入了默认方法,这为接口的演进提供了强大的工具,但也带来了新的风险。
核心原则与最佳实践
- 默认方法是“修正”工具,而非“初始”设计工具
初衷:默认方法的主要目的是允许在接口发布后,以兼容的方式添加新的方法。
错误用法:在初次设计接口时,就默认提供所有方法的实现。这违背了接口定义类型的初衷,可能导致糟糕的API设计。
- 谨慎设计默认方法
实现冲突:如果一个类实现的多个接口有相同的默认方法签名,且没有覆盖该方法,会导致编译错误。
继承问题:默认方法不能感知实现类的状态,因为它只能在接口上定义。它不能访问实例字段。
行为不可预测:默认方法可能被实现类覆盖,其行为不是最终确定的。
例如:
存在两个接口,有相同的默认方法。
public interface TestService {
default void testDefault() {
System.out.println("testDefault");
}
}
public interface Test2Service {
default void testDefault() {
System.out.println("testDefault");
}
}

实现类没有覆盖该方法,会导致编译错误,因为两个接口中有相同的默认方法,实现类也不知道用哪个。
这时我们可以重写覆盖此默认方法,当然也可以通过使用 InterfaceName.super.methodName() 明确指定调用哪个接口的方法。
public class TestServiceImpl implements TestService, Test2Service{
@Override
public void testDefault() {
// 选择使用其中一个
TestService.super.testDefault();
}
}
InterfaceName:指定要调用哪个接口的默认方法
super:表示调用父级(接口)的默认实现
methodName():具体的方法
经典的菱形继承问题,虽然Java只能单继承,但接口可以多实现啊,而且java8接口还引入了默认方法,所以就有了这个问题。
所以,尽量避免使用默认方法添加“可能破坏现有实现”的新方法。只有当新方法有合理且安全的默认实现,并且与接口的核心抽象一致时,才使用默认方法。
- 为每个暴露的接口提供一个抽象骨架类
上一条其实也提高了此方法。这也是本条款中最重要、最实用的设计模式。
概念:
定义一个抽象的类,实现目标接口,并为接口中的方法提供基本的实现(可以是抽象的,也可以是具体的)。这个类被称为骨架实现类(Skeletal Implementation Class),例如 AbstractCollection, AbstractList, AbstractMap。
作用:
- 减少实现负担:实现类可以继承骨架类,只需关注其核心逻辑,无需实现所有接口方法。
- 促进接口演变:在接口中添加新方法时,可以在骨架类中提供一个默认的(甚至是高效的)实现,所有继承该骨架类的实现类将自动获得这个新方法,而无需修改代码。
- 命名惯例:Abstract[InterfaceName],例如 AbstractCollection 对应 Collection 接口。
例如:
// 接口
public interface MyList<E> {
int size();
E get(int index);
// ... 其他方法,如 add, remove 等
}
// 骨架实现类
public abstract class AbstractMyList<E> implements MyList<E> {
// 提供一些方法的默认实现,例如基于 get 和 size 实现迭代
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
private int index = 0;
@Override public boolean hasNext() { return index < size(); }
@Override public E next() { return get(index++); }
};
}
// 其他方法可以标记为抽象,由具体实现类完成
// public abstract void add(int index, E element);
}
// 具体实现类,只需继承骨架类,大大简化
public class SimpleArrayList<E> extends AbstractMyList<E> {
private Object[] elements;
private int size = 0;
@Override public int size() { return size; }
@Override public E get(int index) { return (E) elements[index]; }
// ... 只需要实现核心的抽象方法和自己关心的方法
}
总结
-
设计之初,深思熟虑:假设接口发布后就不能修改,仔细推敲每一个方法。
-
优先使用骨架类:对于复杂的接口,总是提供一个并行的抽象骨架实现类。这是最稳健、最灵活的演进方式。
-
谨慎使用默认方法:
3.1 主要用于接口发布后的兼容性更新。
3.2 确保默认实现是安全、高效且符合逻辑的。
3.3 意识到默认方法可能引起的多重继承冲突。
3.4 提供清晰的文档:告诉用户实现你接口的最佳实践。
3.5 缺省方法不支持从接口中删除方法,也不支持修改现有方法的签名,对接口进行这些修改肯定会破坏现有的客户端代码。
最终目标:通过精心的设计,让你的接口在保持稳定性的同时,具备优雅演进的能力,从而真正地“为后代服务”。

被折叠的 条评论
为什么被折叠?



