组合优于继承的前提是确认“is-a”关系真实成立,否则应避免用继承表达“has-a”或“uses-a”;优先用接口+default方法定义行为契约,抽象类仅在需共享状态或构造控制时使用;警惕深继承链与继承滥用,善用final、private、@Sealed主动约束。
用组合替代继承前先确认「is-a」关系是否真实成立
很多过度继承的根源,是把「has-a」或「uses-a」强行建模成「is-a」。比如 Engine 和 Car,写成 Engine extends Car 就明显违背常识;正确做法是 Car 持有 Engine 实例。判断标准很简单:能否对子类对象说“它**是一种**父类”且不引发逻辑矛盾?如果需要加“在某种上下文中”“勉强算”“为了复用才这么写”,基本就是误用继承。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 每新增一个
extends前,用自然语言造句:“X 是一种 Y”——如果听起来别扭或需要解释,停手 - 检查父类是否真的定义了子类的**本质身份**,而不是仅提供某些可复用行为(后者更适合提取为工具类或接口实现)
- 警惕「为了复用方法而继承」:Java 中
protected方法本意是支持子类扩展,不是为跨领域复用设计的
优先用接口 + 默认方法而非抽象类做能力契约
抽象类容易滑向「继承即复用」陷阱,尤其当它开始包含大量非核心状态或模板逻辑时。接口配合 default 方法能更轻量地表达「能做什么」,且不强制子类共享实现细节或字段布局。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 当多个类需要共享**行为契约**但无共同数据结构时,选
interface;只有当必须共享**状态字段**或**构造流程控制**时,才考虑抽象类 - 抽象类中避免出现与核心职责无关的
protected字段(如cacheMap、retryCount),这类字段往往暴露了设计边界模糊 - JDK 8+ 后,
default方法已支持完整逻辑,多数场景下比抽象类的空方法 + 子类重写更清晰
警惕「继承链过深」导致的脆弱基类问题
超过三层的继承链(如 A → B → C → D)会让修改基类 A 变得高风险:你无法预判某个 protected 方法被哪层子类重写,也无法确定某处 super.xxx() 调用实际执行的是哪个版本。这种不确定性会直接拖慢重构和 bug 定位速度。