继承应严格遵循is-a关系,避免为复用或扩展行为滥用;优先用组合、接口和委托替代过度继承,警惕protected成员滥用及LSP违规。

继承不是万能的,先想清楚“是不是必须用”
Java里过度继承最常出现在把“有某种行为”直接当成“是某种东西”的场景。比如写一个 LoggableUser 类去继承 User,只为了加日志能力——这本质是功能扩展,不是类型层级关系。继承表达的是 is-a 关系,不是 has-a 或 can-do。一旦发现子类只重写一两个方法、其余全是复制父类字段,或者只是为了复用代码而继承,就该警觉了。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 优先用组合:把可复用逻辑封装成独立类(如
Logger、Validator),让User持有它,而不是继承它 - 查一查子类是否真的需要访问父类的
protected成员;如果只调用 public 方法,那大概率该用依赖而非继承 - IDE 里右键看类继承树(IntelliJ 的
Ctrl+H),如果深度超过 3 层或出现“菱形继承”雏形(多个子类继承同一父类又各自派生),就要停下来画 UML 看是否合理
用接口替代抽象父类来约束行为
很多人一想“多个类要共享行为”,第一反应是拉个 BaseService 抽象类,结果越加越多方法,子类被迫实现一堆空方法或抛 UnsupportedOperationException。这是典型的“用继承模拟接口”的反模式。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 把通用行为拆成小粒度接口,比如
Retryable、Transactional、Cacheable,让具体类按需实现,而不是塞进一个大父类 - 抽象类只保留真正共有的状态 + 强制模板逻辑(比如
executeWithLock()中固定包含获取锁、执行、释放锁三步,中间一步由子类实现) - 注意
default方法在接口里的使用边界:它适合提供安全的、无状态的工具逻辑(如formatTimestamp()),但绝不该用来管理实例字段或改变对象生命周期
警惕 protected 字段和“半公开”API
过度继承往往伴随着大量 protected 字段和方法,表面是为子类留扩展点,实际成了紧耦合的温床。子类一旦依赖父类的内部字段,父类连字段名都不敢改,重构立刻崩。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 把
protected字段全换成 private,提供受控的protectedgetter/setter(且只在明确需要被子类定制时才暴露) - 检查所有
protected方法:它是否真的需要被外部继承?能不能改成 package-private + 同包内委托类? - 用
@Deprecated标记过时的protected成员,并在注释里写清替代方案(比如“请实现CustomProcessor接口”)
从 Liskov 替换原则倒推设计缺陷
如果某段代码接收 Animal 类型参数,传入 Dog 没问题,但换成 Ostrich 就抛异常(比如因为 Ostrich.fly() 直接 throw new UnsupportedOperationException),说明继承体系违反了 LSP——Ostrich 并不能真正替代 Animal。这种问题在测试里往往藏得深,直到上线后某个分支路径才暴露。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 对每个继承链,写一个“子类能否无感替换父类”的单元测试:用子类实例跑父类的所有 public 方法测试用例
- 如果子类必须覆盖父类方法并大幅改变语义(比如父类
save()是同步写库,子类改成发 MQ),那它就不该是继承关系,而是策略注入 - 遇到“部分子类不支持某功能”的情况,别加
if (this instanceof X)分支,直接拆接口:让支持者实现Savable,不支持者不实现
继承的边界模糊性,常常不是写代码时决定的,而是第一次有人为了“少写几行”而继承一个不该继承的类时埋下的。真正的控制点不在语法层面,而在每次 extends 前多问一句:这个子类,删掉父类后还能不能独立存在、清晰表达自己的职责?










