组合优于继承,关键在于判断是否为真正的“is-a”关系;若仅为代码复用或职责不同,应选组合以降低耦合、提升可测性与可扩展性;仅当满足里氏替换且存在共享行为时才用继承。

组合优于继承,这是Java设计中被广泛接受的原则。选择的关键不在于“能不能用继承”,而在于“这个关系是不是真正的‘is-a’”。如果只是想复用代码,组合更灵活、低耦合、易测试;只有当子类在逻辑上确实是父类的一种时,继承才合适。
看语义:先问“它是不是一种?”
继承表达的是类型之间的层级关系,必须满足里氏替换原则——任何使用父类的地方,都能无缝替换为子类。比如red">"Dog is a Animal"成立,适合继承;但"Car has an Engine"不是“是一种”,而是“拥有一个”,这时该用组合。
- 如果把“ReportGenerator”继承“FileWriter”,就混淆了职责——它不是一种文件写入器,只是需要写文件
- 若强行用继承,后续想换输出方式(如写到数据库或发HTTP请求)就得改继承结构,破坏开闭原则
- 用组合的话,只需注入不同的Writer实现,行为可插拔,结构稳定
看变化:哪个部分更可能变?
继承让子类与父类紧绑定,父类一改,所有子类都可能受影响;组合则把变化封装在独立对象里,修改内部实现不影响外部接口。
- 比如“订单服务”需要计算运费,用继承意味着把计费逻辑写进基类——一旦促销规则、地区策略、物流渠道频繁调整,基类会越来越臃肿
- 改用组合:定义ShippingCalculator接口,不同实现(FlatRate、WeightBased、FreeForVIP)可自由切换,测试也只需mock接口
- 运行时甚至能根据订单属性动态选择策略,继承做不到这点
看测试与维护:谁更容易单元测试?
继承结构下,子类测试常需启动整个继承链,依赖父类状态和构造逻辑,容易出现“测试即集成”的问题;组合天然支持依赖注入,便于隔离测试。
立即学习“Java免费学习笔记(深入)”;
- 一个继承自JFrame的GUI类,单元测试时会尝试创建真实窗口,失败率高且慢
- 若改为组合:把业务逻辑抽成独立Service类,GUI只负责展示和转发事件,Service可纯内存测试
- Mock掉数据库、网络等外部依赖后,核心逻辑验证清晰、快速、可靠
特殊情况:继承仍不可替代
并非完全弃用继承。以下场景中,继承仍有其价值:
- 需要多态分发且共享大量通用实现,如AWT/Swing组件体系(Button、Label都继承Component,复用事件处理、布局、绘制等基础能力)
- 框架要求扩展特定抽象类(如Spring的InitializingBean、Hibernate的UserType),此时继承是契约的一部分
- 构建领域模型中的明确分类层次,如PaymentMethod → CreditCard、PayPal、CryptoWallet,且各子类确实共享核心行为和约束










