该用 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,让WechatStrategy、AlipayStrategy各自实现 -
OrderService持有private PaymentStrategy strategy,通过 setter 或构造器注入 - 灰度发布时,可以按用户 ID 哈希动态选策略,完全不改类结构,也不触发重新部署
继承还没死,但它真的只适合极少数情况
别一看到 extends 就删,JDK 自己还在用。关键是看父类是否明确设计为被继承:有没有文档说明「供子类扩展」?有没有 protected 钩子方法?是否实现了模板方法模式?
立即学习“Java免费学习笔记(深入)”;
-
ArrayList继承AbstractList是合理继承——后者用abstract get(int)强制子类提供底层访问,且所有public方法都封装了通用逻辑 - 但如果你写的
BaseController里只有public void render()和一堆protected工具方法,那它其实是个工具类,不该被继承,而该被组合 - 更危险的是:父类没加
final,但语义上根本不允许重写(比如save()内部已包含事务和校验),这时子类 override 就等于撕毁契约
extends——因为那行代码当时看起来最省事。而代价,往往半年后才浮现。










