优先使用组合而非继承:组合支持运行时替换行为、便于测试和解耦;继承适用于稳定 is-a 关系且无业务语义的模板类,如 abstractlist;滥用继承会导致僵化与维护困难。

继承写多了,编译期就卡死,运行时改不动
继承关系在 Java、C# 等语言里是编译期绑定的:子类一写 extends Parent,父类结构就锁死了。一旦 Parent 里加个 protected 方法或改个构造逻辑,所有子类都得跟着测——哪怕它们根本不用这个功能。
组合则完全相反:class Bird { private FlyBehavior flyBehavior; },flyBehavior 可以在运行时换成 new RocketFly() 或 new NoFly(),连重新编译都不用。
- 多层继承(比如
A → B → C → D)会让调试像扒洋葱:改一行B的代码,D的行为突然异常,但堆栈里根本看不出关联 - 想给已有类“动态加能力”?继承做不到;组合只要换掉成员对象就行
- 单元测试时,继承让 mock 变得困难——你得 mock 整个父类链;组合只需 mock 一个接口实现
is-a 还是 has-a?别硬套,先看领域语义
很多人误以为“鸭子是鸟,所以 Duck extends Bird”天经地义。但现实里,Bird 如果定义了 fly(),那企鹅和鸵鸟就只能重写成抛异常——这不是建模,是埋雷。
真正该问的是:这个类“需要什么行为”,而不是“它属于哪个分类”。fly() 不是鸟的本质属性,而是可插拔的能力。
- 当语义是“是一个”且稳定不变(如
IOException extends Exception),继承合理 - 当语义是“有一个”“能做某事”(如
Order有PaymentStrategy、有NotificationService),必须用组合 - 看到抽象类名带
Abstract+Base+ 多层派生(如BaseDao → JdbcBaseDao → MySqlBaseDao),大概率是组合没用好
Java 里怎么写组合?不是 new 就完事
组合不是简单在类里 new 一个对象,关键在解耦和可替换。直接 private DatabaseHelper helper = new DatabaseHelper(); 和继承一样僵硬。
正确做法是依赖接口或抽象类型,把具体实现推迟到外部注入:
class UserService {
private final UserRepository userRepository; // 接口,非具体类
UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}- 永远优先声明为接口(
UserRepository)而非实现类(JdbcUserRepository) - 构造器注入优于
new,便于测试和替换 - 避免在组合对象里暴露内部细节:不要让
UserService调用userRepository.getJdbcTemplate()
什么时候真该用继承?别被教条吓住
组合不是银弹。有些场景继承更轻量、更直观,强行组合反而绕远路。
典型例子:ArrayList 继承 AbstractList,HashMap 继承 AbstractMap——这些基类只提供通用骨架,不带业务语义,也不对外暴露 protected 实现细节,改动风险极低。
- 父类是
final或纯模板方法(templateMethod()调用抽象doSomething()),且无状态、无副作用 - 子类数量极少(≤3),且生命周期与父类强绑定(如 Swing 组件
JButton extends JButton) - 性能敏感路径(组合多一层虚方法调用,但现代 JVM 基本优化掉了,除非在 nanotime 级别压测)
真正难的不是选组合还是继承,而是识别出那个“不该被继承”的父类——它往往已经悄悄长出了三个以上 protected 方法,还被五个子类各自重写了不同逻辑。








