该用组合而非继承当子类仅为复用代码且不满足“is-a”关系时,如Car与Engine;常见错误包括ClassCastException、空方法重写、父类修改引发崩溃;组合通过委托、接口和依赖注入实现多态与策略切换,更安全可控。

什么时候该用组合而不是继承
继承不是不能用,但一旦子类只为了复用代码而存在,又不真正符合“is-a”关系,就该立刻警觉。比如 Engine 和 Car:Car 有发动机,不是“是一种发动机”,所以不该让 Car extends Engine。
常见错误现象:ClassCastException、子类被迫重写大量空方法、修改父类导致下游意外崩溃。
- 当父类行为不稳定(比如频繁改
protected方法逻辑),用组合能隔离变化 - 需要运行时切换行为(比如换不同
PaymentStrategy),继承做不到,组合配合接口可以 - 想测试某个功能模块,继承会让测试必须搭起整个类层次,组合只需 mock 掉依赖对象
怎么把继承改成组合:三步替换法
核心是把“继承来的字段和方法”转成“持有的对象 + 委托调用”。不是简单加个字段就完事,重点在控制权转移。
假设原代码是 class AdminUser extends User,现在要重构:
立即学习“Java免费学习笔记(深入)”;
- 删掉
extends User,声明私有字段private User user; - 在构造器里初始化
user = new User(...);,或通过参数传入(利于测试) - 把原来直接调用的
getName()、getRole()等方法,改成return user.getName();—— 这叫委托(delegation),不是透传
注意:别偷懒写 public User getUser() { return user; },这等于把封装捅了个洞,外部能绕过你的控制逻辑直接操作 user。
组合中如何处理多态和策略切换
继承天然支持多态,组合得靠接口+依赖注入。关键不是“能不能”,而是“谁负责决定用哪个实现”。
比如日志模块,原来用 FileLogger extends Logger、DbLogger extends Logger,现在改为:
interface LogSink {
void write(String msg);
}
class FileLogSink implements LogSink { ... }
class ConsoleLogSink implements LogSink { ... }
class Service {
private final LogSink sink;
Service(LogSink sink) { this.sink = sink; } // 运行时决定
}
性能影响很小,反而是更可控:避免继承树过深带来的方法解析开销;兼容性也更好——旧代码可继续用继承版,新模块统一走组合接口,逐步迁移。
容易踩的坑:Service 如果自己 new FileLogSink,就又写死了;必须由上层(比如 Spring 容器或工厂类)注入,否则组合就退化成硬编码。
为什么 IDE 不会帮你自动完成这个重构
因为组合不是语法转换,而是语义重构。IDE 可以把 extends 删掉、加字段、生成委托方法,但它没法判断:User 的哪些方法该暴露,哪些该拦截加工,哪些该拒绝调用。
真正难的是设计决策点:
-
AdminUser调用user.delete()是否合法?继承下无法阻止,组合下你可以在委托前加校验 - 是否需要对
user.getName()返回值做脱敏?组合让你有一层自由加工的空间 - 多个组合对象之间要不要共享状态?比如
CacheManager和MetricsCollector是否共用同一个Timer实例?继承完全没这个视角
这些都得人来想清楚边界。工具只能搬砖,砌墙的方向得自己定。








