优先使用组合而非继承,因组合支持运行时替换、降低耦合、符合单一职责;继承仅适用于满足“是”关系且父类明确设计为可继承的场景。

组合优于继承不是一句空话,它直接关系到代码是否容易维护、测试和替换。核心就一条:当两个类之间是“有一个”(has-a)关系,而不是“是一种”(is-a)关系时,优先用组合。
什么时候该选组合而不是继承
判断标准很实在:子类能不能自然地被说成父类的一种?
- Car has an Engine → 用组合
- Dog is a Animal → 可以用继承
- Stack is a Vector?不成立,Stack只是用了Vector的能力 → 必须用组合
- PaymentService uses AlipayClient 或 WechatPayClient → 组合,且可随时切换
组合的正确写法:private final + 构造器注入
别让组合对象在类里随便 new,也别暴露出去。
- 字段声明为 private final List
storage; - 通过构造器传入,比如 public CacheService(List
backend) - 构造器里用 Objects.requireNonNull(backend) 拦住空值,别等运行到业务逻辑才崩
- 不提供 public setStorage() 方法,除非真要支持运行时热替换(比如灰度切支付渠道)
为什么继承 Vector 实现 Stack 是典型反例
Java 早期的 Stack 类继承自 Vector,结果带来一堆问题:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
- Stack 意外获得 Vector 的 add()、remove(int)、get(int) 等全部方法,语义混乱
- 所有操作都带同步开销,但栈本身并不需要线程安全
- 底层容器被锁死,没法换成 LinkedList 或带 LRU 的自定义实现
- Vector 内部 protected 方法一旦变更(比如删掉 ensureCapacity),Stack 就可能编译失败
正确做法是内部持有一个 private final List
哪些继承场景看着合理,其实暗藏风险
不是所有 is-a 都适合继承,尤其当父类没为你设计好扩展点时:
- 继承 HashSet 并重写 add() 来统计添加次数?小心漏掉对内部 HashMap 的操作,导致 size() 不准
- 继承 String 或 LocalDateTime?它们被设为 final,不是为了防扩展,而是状态契约太脆弱,子类根本守不住
- 父类 JavaDoc 没写 “This class is designed for inheritance”,却有大量 private 方法和 final 方法 → 基本等于拒绝继承
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










