应优先选择组合而非继承,因继承要求严格的is-a关系并满足里氏替换原则,而组合更灵活、松耦合、易测试;若非真正“是一种”,或父类未设计为可继承,则必须用组合,并通过接口依赖、构造注入和行为委托实现。

选组合还是继承,关键不在能不能写出来,而在于关系是否真实成立——用错关系,耦合自然升高;用对了,结构才松、可测、易换。
先问一句:“它是不是一种?”
继承表达的是 is-a 关系,且必须满足里氏替换原则:任何用父类的地方,换成子类不能出错。比如 Dog extends Animal 合理,因为狗确实是一种动物;但 Order extends Exportable 就不合理——订单不是“导出物”的一种,它只是“能导出”。这类能力应抽成接口,让 Order implements Exportable。
如果两个类名之间读不出“是一种”,但能自然说出“有一个”或“依赖一个”,那就该用组合。常见信号包括:
- 复用目的只是调用某功能,而非扩展类型体系
- 需要运行时切换实现(如测试时用 Mock,生产切不同支付渠道)
- 类名之间逻辑上不构成分类关系(比如
Car extends Engine明显荒谬)
再看父类:“它欢迎被继承吗?”
即使语义成立,继承也不一定安全。Java 中大量类并未设计为可继承,强行 extends 容易埋雷:
- 父类 JavaDoc 没写 “This class is designed for inheritance”
- 关键方法是
final,或内部强依赖private字段和隐式调用顺序 - 像
String、LocalDateTime这类不可继承,不是限制你,而是状态契约太脆弱 - 有大量
protected成员,但文档没说明哪些可重写、哪些不能动
一旦发现这些迹象,哪怕“Dog is a Animal”再合理,也要改用组合。
组合要真“组合”,不是塞个字段就完事
组合不是简单地在类里加个 new Xxx(),关键是委托行为 + 控制可见性:
- 用
private final字段持有依赖,通过构造器注入,避免 null 和后期篡改 - 对外只暴露你需要的能力,不泄漏底层实现(比如
Stack不该暴露add()、get()等List接口) - 依赖尽量声明为接口(如
DataSource而非MySQLDataSource),方便后续替换成 Redis 或 Mock - 避免深度调用链,比如
a.getB().getC().doX(),这会把空指针风险和耦合层层传递
把共性能力从继承里“摘出来”
日志、缓存、校验、序列化、重试这些功能,常在多个类中重复出现,但和核心业务身份无关。它们不该硬塞进父类,而应抽成接口+小类:
- 定义
Logger接口,再写FileLogger、RemoteLogger实现 - 把
JsonSerializer、XmlSerializer分开,每个只做一种格式转换 - 避免“工具类大杂烩”,一个类别同时管校验、缓存、加密
多个不相关类需共享逻辑时,用接口 + default 方法,比硬造共同父类更干净。比如:
interface Loggable { default void log(String msg) { System.out.println(msg); } }Cat、Order、Report 各自 implements Loggable,按需覆盖即可。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











