Java中应优先用组合替代继承,当关系是“有一个”而非“是一种”时更合理;典型重构信号包括子类仅重写少量方法、硬凑is-a关系、继承链过深、父类暴露protected成员、测试依赖整条继承链;三步重构:抽离接口、持有组件、委托调用;组合需配合接口抽象与依赖注入,并通过定义小粒度接口或包装器模式保障多态兼容性。

Java 中继承不是不能用,而是很多场景下用组合更合理。关键不是“去掉 extends”,而是判断关系是否真是“是一种”,还是“有一个”。比如 FileLogger 不是一种 Logger,只是用到了日志能力;Ostrich 不是一种能飞的 Bird,只是属于鸟类的一种——这类情况,组合就是更自然、更安全的选择。
识别该换组合的典型信号
遇到以下情况,说明继承可能已偏离本意,该考虑重构了:
- 子类只重写一两个方法,其余全靠父类默认实现(父类塞了太多不相关的职责)
- 为了复用代码硬凑 is-a 关系,比如
ReportGenerator extends DatabaseConnection(它显然不是连接,只是需要连接) - 继承链超过两层(A → B → C),改 C 却意外影响 A,而 A 和 C 在业务上几乎无关
- 父类大量暴露
protected方法或字段,子类靠“偷看”内部逻辑才能工作(破坏封装) - 测试时必须把整个父类初始化链拉起来,只为验证子类一行逻辑
三步完成从继承到组合的重构
以 FileLogger extends Logger 为例,实际它只是“持有格式化器和文件写入器”,并非 Logger 的子类型:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
-
抽离可复用行为为接口或组件:提取
LogFormatter接口(含format())和LogWriter接口(含write()),再提供SimpleFormatter、FileWriter等具体实现 -
让类持有组件,不再继承:新建
FileLogger类,内部声明private final LogFormatter formatter;和private final LogWriter writer; -
委托调用,清晰表达意图:在
log(String msg)中,先formatter.format(msg),再writer.write(...),不隐藏、不绕弯
让组合真正灵活的关键做法
组合的价值,只有配合接口抽象和依赖注入才能释放:
立即学习“Java免费学习笔记(深入)”;
- 用构造函数注入依赖:
public FileLogger(LogFormatter f, LogWriter w),语义明确、不可变、易测试 - 单元测试时,直接传入
MockFormatter和InMemoryWriter,无需 mock 整个继承链 - 新增
CloudLogger?只需换一个CloudWriter实现,其余逻辑零修改 - 若需运行时切换策略(如按环境选不同格式器),用策略模式包装,仍是组合,不是继承
组合后如何处理类型统一与兼容性
继承天然支持多态,组合则需主动设计类型契约:
- 定义顶层接口(如
Logger),所有组合类都实现它,而非继承某个抽象基类 - 避免大而全的接口;按能力拆小接口,例如
SyncLogger、AsyncLogger,组合类按需实现 - 若旧系统强依赖继承结构(如 Spring Bean 冲突),可用包装器模式:
LegacyLoggerWrapper extends AbstractLogger,内部持组合对象并转发调用 - 运行时类型判断别依赖
getClass().isAssignableFrom(),改用接口检查或字段特征识别

















