该用继承而非组合当且仅当存在明确“is-a”关系且需共享状态行为,如E是EBase的具体类型;若为“has-a”关系(如User拥有Address)则必须用组合,误用会导致接口污染与维护风险。

什么时候该用继承而不是组合
当多个类之间存在明确的“is-a”关系,且需要共享状态和行为时,才适合用继承。比如 EBase 是所有实体的基类,E 是它的一个具体实现类型,这种语义上“E 是一种 EBase”就成立;但若只是“User 拥有一个 Address”,那就该用组合——强行继承会导致子类暴露不该有的父类接口,后续改字段或加约束时牵一发而动全身。
常见误用场景:
- 把“有”关系硬写成“是”关系(如
Order继承Address) - 为复用工具方法而让业务实体继承
UtilsHelper类(应提取为静态工具或依赖注入) - 子类只重写一两个方法,其余 90% 都是空实现(说明抽象粒度太粗,该拆)
JPA 单表继承中 discriminator 字段必须显式配置
用 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 时,Hibernate 不会自动加区分字段。漏掉 @DiscriminatorColumn 会导致所有子类数据混存、查询时无法反序列化到正确类型,运行时报 org.hibernate.WrongClassException。
正确写法要点:
-
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)中的name必须与数据库实际列名一致 - 每个子类必须标注
@DiscriminatorValue("E"),值不能重复,也不能为 null - 如果已有表结构,新增 discriminator 列后需补全历史数据的 type 值,否则老记录查不出来
protected 字段 + public getter 是安全的持久化模式
直接把字段设为 public 会破坏封装,设为 private 又会让 JPA 无法反射赋值(除非加 @Access(AccessType.PROPERTY))。最稳妥的是用 protected 字段配 public getter/setter,既满足 Hibernate 的字段访问要求,又保留了逻辑控制入口。
例如:
protected String name;
public String getName() { return name; }
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("name 不能为空");
}
this.name = name.trim();
}
这样在构造、更新、甚至 JSON 反序列化时都能统一校验,避免脏数据进库。
Builder 模式更适合不可变实体或构造即完整的场景
如果实体创建后不允许修改(如订单快照、审计日志),或者构造参数太多、可选字段多,就别用继承链来堆逻辑。此时 Builder 更清晰:每个子类自己定义构建规则,不依赖父类构造器顺序,也不用处理 super(...) 传参错位问题。
典型适用点:
- 领域事件对象(如
OrderCreatedEvent)一旦发出就不能改 - DTO 或 API 请求体,字段来自不同上下文,组合逻辑复杂
- 测试中需要大量不同组合的实例,用 Builder 写法比 new 多个子类更轻量
继承不是银弹,真正难的不是语法怎么写,而是判断哪些共性该抽到父类、哪些该留给子类自己决定——这个边界一旦划错,后面每加一个新类型,都要回过头去动基类,改一次就多一分风险。

















