继承树过深导致难改难测,因子类对父类隐式依赖强;组合通过抽接口、提字段、委托调用解耦,但需权衡is-a语义、类型兼容与性能等场景。

为什么继承树一深就难改又难测
继承关系越深,子类对父类的隐式依赖就越强。改一个 protected 方法,可能让下游十几个子类行为突变;加个新子类,得反复确认它有没有无意覆盖了关键钩子方法;写单元测试时,还得把整个父类初始化链拉起来——哪怕你只关心子类里一行逻辑。
组合能切断这种“牵一发而动全身”的耦合。把功能拆成独立、可替换的部件,用字段持有它们,而不是靠 extends 继承层级来拼凑行为。
常见错误现象:NullPointerException 频繁出现在父类构造器里调用被子类重写的 hook 方法(因子类字段尚未初始化);final 类无法扩展,但已有继承体系又不允许重构;mock 测试时发现不得不 mock 整个父类链。
怎么把一个继承结构快速转成组合
核心动作就三步:抽接口、提字段、委托调用。不追求一步到位,先从最常变、最易错的那块逻辑下手。
立即学习“Java免费学习笔记(深入)”;
- 找出继承树中变化最频繁的职责(比如日志策略、序列化方式、重试逻辑),把它定义为接口,如
RetryPolicy、Serializer - 在原基类或目标类中,删掉
extends,新增字段,如private final RetryPolicy retryPolicy - 把原来由父类实现、子类定制的方法,改成调用该字段,如
retryPolicy.shouldRetry(exception) - 构造器里传入具体实现,支持运行时替换(别用
new DefaultRetryPolicy()硬编码)
示例:原 HttpClient 继承 BaseClient,再继承 NetworkClient;现在直接在 HttpClient 里持有一个 NetworkLayer 字段,所有网络调用都委托给它。
组合后怎么处理“is-a”语义和类型兼容问题
继承天然表达 is-a,组合则更倾向 has-a。但业务代码里常需要统一类型处理,比如 List<client></client> 里混放不同客户端。
解决路径很实际:
- 保留顶层接口(如
Client),让所有组合类实现它,而非继承某个抽象类 - 避免在接口里塞太多方法;按场景拆小接口,如
SyncClient、AsyncClient,组合类按需实现 - 如果旧代码强依赖继承体系(比如 Spring 的
@PrimaryBean 冲突),可用包装器模式:新建一个LegacyClientWrapper实现原抽象类,内部持组合对象并转发调用 - 注意泛型擦除下
Class.isAssignableFrom()不再成立,运行时类型判断要改用字段/接口检查
哪些情况别硬套组合,小心过早抽象
不是所有继承都该被消灭。组合带来灵活性的同时,也增加间接层和对象数量。
以下情况保持继承更合理:
- 类之间确实是严格的 is-a 关系,且行为差异仅在于少量参数(如
ArrayList和LinkedList都是List,但底层数据结构不可互换) - 框架强制要求继承(如 Android 的
Activity、JUnit 的TestCase),此时组合只能作为内部优化手段,外部仍需继承 - 性能敏感路径,频繁委托调用引发可观开销(如高频循环里的
formatter.format()),这时内联或模板方法更合适
最容易被忽略的一点:组合不等于“每个行为都做成接口+实现类”。很多逻辑其实只需一个配置枚举或函数式接口参数,比如 Comparator<t></t> 或 Consumer<string></string> —— 它们比定义十个 LogStrategy 子类轻量得多。








