17370845950

在Java里为什么说组合优于继承_Java对象组合设计原则解析
该用 private final 字段组合而非 extends:当子类仅需复用逻辑而非表达“是某种东西”时,如 OrderService 拥有日志、支付能力,而非本身就是 Logger 或 PaymentGateway。

什么时候该用 private final 字段组合,而不是 extends

当你发现子类只是想复用父类的某段逻辑,而不是在表达“它本质上就是那种东西”时,就该停手了。比如:OrderService 需要发消息、记日志、做支付——它不是一种 Logger,也不是一种 PaymentGateway,它只是「拥有」这些能力。

  • extends Logger 会让 OrderService 暴露 logError()logDebug() 等本不该由业务服务直接对外提供的方法
  • extends 还会强制继承父类的初始化顺序、protected 字段,甚至可能被子类意外重写关键方法(如 send() 被绕过重试逻辑)
  • 而组合 + private final Logger logger,既锁定了依赖不可变,又只暴露你主动委托的接口(比如只调 logger.info()

如何用构造函数注入避免 NullPointerException

组合不等于随便 new 一个对象塞进去;没管好生命周期,运行时崩得比继承还快。最常见错误是字段为 null,尤其在 Spring 环境下误用 @Autowired 字段注入,导致测试时无法手动传参。

  • 永远优先用构造函数注入:把依赖声明为 private final,并在构造器里强制接收,编译期就能挡住空值
  • 配合 @NonNull(Lombok 或 JetBrains 注解)或显式 Objects.requireNonNull() 校验
  • 别让组合对象自己 new 自己(比如在 OrderService 构造器里 new PaymentProcessor()),这会破坏可测性与替换能力
public class OrderService {
    private final PaymentProcessor processor;
    private final Logger logger;

    public OrderService(@NonNull PaymentProcessor processor, @NonNull Logger logger) {
        this.processor = Objects.requireNonNull(processor);
        this.logger = Objects.requireNonNull(logger);
    }
}

为什么策略模式是组合最自然的落地场景

当你要支持「运行时切换行为」,比如支付渠道从微信切到支付宝,或风控规则从宽松切到严格,继承立刻卡死——你不可能让一个对象同时是 PayWithWechat 又是 PayWithAlipay

  • 定义 interface PaymentStrategy,让 WechatStrategyAlipayStrategy 各自实现
  • OrderService 持有 private PaymentStrategy strategy,通过 setter 或构造器注入
  • 灰度发布时,可以按用户 ID 哈希动态选策略,完全不改类结构,也不触发重新部署

继承还没死,但它真的只适合极少数情况

别一看到 extends 就删,JDK 自己还在用。关键是看父类是否明确设计为被继承:有没有文档说明「供子类扩展」?有没有 protected 钩子方法?是否实现了模板方法模式?

立即学习“Java免费学习笔记(深入)”;

  • ArrayList 继承 AbstractList 是合理继承——后者用 abstract get(int) 强制子类提供底层访问,且所有 publ

    ic
    方法都封装了通用逻辑
  • 但如果你写的 BaseController 里只有 public void render() 和一堆 protected 工具方法,那它其实是个工具类,不该被继承,而该被组合
  • 更危险的是:父类没加 final,但语义上根本不允许重写(比如 save() 内部已包含事务和校验),这时子类 override 就等于撕毁契约
真实项目里最难的不是写对组合,而是说服同事别在新功能里随手加一层 extends——因为那行代码当时看起来最省事。而代价,往往半年后才浮现。