组合优于继承的前提是确认“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 定位速度。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- IDE 中打开类层级视图(IntelliJ:
Ctrl+H),定期检查继承深度;超过 2 层即触发审查 - 将中间层(如
B、C)拆解为独立组件,通过构造函数注入到顶层类(D)中,用组合显式声明依赖 - 若必须保留多层,确保每层只解决一个明确问题(如
NetworkClient → HttpNetworkClient → AuthenticatedHttpNetworkClient),且每层都提供不可绕过的语义价值
用 final 类和 private 方法主动封禁继承滥用
不是所有类都该被继承。工具类(StringUtils)、值对象(LocalDateTime)、配置载体(DataSourceConfig)一旦开放继承,极易被误用于扩展业务逻辑,破坏封装性。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 新写的工具类、DTO、枚举、记录类(
record)默认加final修饰符 - 抽象类中,把不希望子类覆盖的方法声明为
private或final;模板方法模式中,钩子方法(hookXXX())用protected,骨架逻辑用final - 使用
@Sealed(Java 17+)限制哪些类可以继承当前抽象类,比无约束extends更可控
// 示例:用 sealed 限制继承范围
public abstract sealed class Shape permits Circle, Rectangle, Triangle {
public abstract double area();
}
// Circle 必须显式声明 extends Shape,且不能被其他类继承
final class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI radius radius; }
}
真正难的不是写出不编译错误的继承,而是每次按 extends 键前,心里清楚这个关系会不会在三个月后让接手的人皱眉。










