java中组合与继承的选择取决于语义关系和演进需求:语义上“有一个”用组合,“是一个”用继承;演化上需确认父类是否明确支持继承;组合通过委托实现解耦与单一职责,更可控、易测试、易替换。

Java 中组合与继承不是“选哪个更高级”,而是“哪个更贴合语义和演进需求”。关键不在语法怎么写,而在类之间真实存在的关系是否稳固、可维护。
先看语义:是“有一个”还是“是一个”
这是最快速的判断起点,不靠经验,靠读出来是否自然:
- “订单有一个支付处理器” → 用组合,
Order类持有PaymentProcessor实例 - “金毛犬是一种狗” → 用继承,
GoldenRetriever extends Dog - “汽车有一个发动机”不能写成
Car extends Engine——这既违反常识,也破坏封装,Engine 的启动逻辑不该暴露给 Car 的所有方法
再看演化:父类是否真正为你开放继承
即使语义成立,“Dog 是 Animal”没错,但继承仍可能不安全。必须确认三点:
- 父类 JavaDoc 明确写着 “This class is designed for inheritance”
- 关键方法不是
final,且没有隐式契约(比如重写toString()导致 JSON 序列化异常) - 父类没有暴露
protected字段或内部状态(如HashSet的map字段),否则子类一改就崩
像 String、LocalDateTime 这类类,哪怕语义上“日期时间是一个对象”,也不该继承——它们内部状态复杂,子类无法保证不变量。
重点看解耦:组合如何让代码更可控
组合的核心价值不是“多写几行 new”,而是通过委托(delegation)把责任切分清楚:
- 构造时注入依赖:
new DatabaseService(new MockJdbcDao()),测试不用启动数据库 - 运行时替换实现:
service.setDao(new RedisCacheDao()),无需改业务逻辑 - 避免继承链污染:
super.super.method()调用一旦父类重构,下游全挂;组合对象只调自己的接口,影响面窄
Spring 默认推荐构造注入而非继承,正是基于这个逻辑:控制权在使用者手里,不在父类设计者手里。
最后看职责:一个类到底该承担多少事
继承容易让子类“被迫继承不需要的能力”,组合则天然支持单一职责:
- 如果一个类既要管理数据、又要渲染界面、还要处理网络请求——它大概率该拆,而不是靠继承多个父类(Java 还不支持)
- 用组合,你可以让
ReportGenerator持有DataFetcher、Renderer、Exporter各一个,每个只做一件事 - 每个成员变量对应一个明确契约,接口定义清晰,mock 和替换都直观
不复杂但容易忽略:组合不是堆字段,而是有意识地划分边界、定义委托关系。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











