组合优于继承是解决实际问题的务实选择,核心在于区分is-a与has-a语义:继承仅用于“X是Y的一种”,组合用于“X有一个Y”,通过private final+构造器注入实现依赖显式化、接口聚焦与生命周期可控。

组合优于继承不是一句空话,而是解决实际问题的务实选择。核心在于:当两个类之间不是“是一种”(is-a)关系时,就别用 extends,改用“有一个”(has-a)的方式把能力装进来。
明确区分 is-a 和 has-a 场景
判断起点永远是语义:
-
用继承:只有当你能自然说出“X 是 Y 的一种”且不会引起逻辑矛盾时——比如
AdminUser extends User、ArrayList implements List(接口实现本质也是契约意义上的 is-a); -
用组合:当 X 拥有 Y 的能力,但 X 并不“是”Y——比如
OrderService有一个PaymentProcessor,Car有一个Engine,Stack有一个List。
用 private final + 构造器注入实现组合
这是最稳妥、最易测试的写法:
- 声明为
private final PaymentStrategy paymentStrategy;,避免外部篡改或空状态; - 在构造器中强制传入,并用
Objects.requireNonNull()校验,让错误暴露在对象创建阶段; - 不提供
setPaymentStrategy(),除非业务真需要运行时切换(如灰度发布); - 不直接
new PaymentStrategyImpl(),把实例创建权交给上层(如 Spring 容器),便于替换和 mock。
把可变部分抽成接口,而非继承具体类
避免子类被父类实现细节绑架:
在调试 Java 后端 API 故障时使用此技能,尤其适用于以下任务:分析日志、跨 Controller、Service 和 Mapper 层追踪请求、定位异常根因、检查 SQL 执行或参数流转,以及生成简洁的诊断结论与修复方案。
- 不要继承
LinkedHashMap写缓存,而是定义CachePolicy接口,让LruPolicy、FifoPolicy各自实现; - 不要继承
Vector实现Stack,而应组合一个List,只暴露push()和pop(); - 接口命名要反映业务意图,比如用
NotificationChannel而不是INotifier,让调用方一眼看懂职责。
警惕继承的隐性风险,该拦就拦
看到这些信号,就该停下继承设计:
- 父类没标注
@MustOverride或 JavaDoc 里没写“designed for inheritance”; - 子类重写方法时必须记得调
super.xxx(),否则日志、权限校验就丢; - 想加个新行为,得横跨多个继承层级修改,或者要硬塞一个中间抽象类;
- 遇到
Cannot inherit from final 'String'这类编译错误——别绕,直接包装。
组合不是套个字段就完事,关键是把依赖显式化、接口职责聚焦、生命周期可控。它不排斥继承,只是把继承留给真正适合的地方:框架契约要求时、语义清晰的 is-a 关系、或模板方法模式这种受控扩展场景。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










