子类不该覆盖父类的public方法,除非明确设计为可重写;应优先用组合代替继承,避免构造器中调用可重写方法,并根据是否需共享状态选择接口或抽象类。

子类不该覆盖父类的 public 方法,除非明确设计为可重写
Java 中 public 方法默认是“开放扩展点”,但多数业务类并非为继承而设计。如果父类没用 @Override 注解标记、也没在 Javadoc 里写明“此方法可被子类重写”,那它大概率不是契约的一部分。
常见错误现象:NullPointerException 出现在子类重写的 toString() 里,只因父类构造器中调用了该方法(此时子类字段尚未初始化);或重写 hashCode() 却没同步改 equals(),导致 HashSet 行为异常。
- 判断依据:看父类是否声明了
final,或是否在构造器中调用该方法 - 安全做法:把可重写的方法显式标为
protected,或加@Override+ 完整 Javadoc 说明契约 - IDE 提示不等于安全——IntelliJ 默认允许重写
public方法,但javac不校验语义合理性
用组合代替继承,尤其当“is-a”关系不稳固时
比如 Employee 类继承 Person 看似合理,但一旦业务要求“兼职学生员工”同时有 Student 和 Employee 属性,单继承立刻失效。这时 Employee 应持有 Person 实例,而非继承它。
使用场景:需要复用逻辑但又不想暴露父类接口、需运行时切换行为(如策略模式)、或父类来自第三方 SDK(无法修改)。
立即学习“Java免费学习笔记(深入)”;
- 组合的关键是把依赖声明为
private final Person person;,而非extends Person - 不要为了“少写几行代码”而选继承——多写几个委托方法(
getName() { return person.getName(); })换来的是长期可维护性 - 注意 Liskov 替换原则失效的信号:子类必须重写父类某个方法才能让逻辑正确,这往往意味着继承关系建模错误
抽象类 vs 接口:优先选接口,除非需要共享状态或默认实现逻辑
Java 8+ 允许接口含 default 方法,但接口仍不能定义实例字段。如果多个子类必须共用一个 private int retryCount; 或缓存 Map,抽象类更合适;否则一律用接口。
参数差异:interface 支持多实现,abstract class 只能单继承;接口方法默认 public abstract,抽象类方法可设 protected 或包私有。
- 性能影响极小,但接口更利于 mock 测试(无需构造抽象类实例)
- 避免在接口里塞太多
default方法——它容易演变成“伪抽象类”,破坏接口的契约纯粹性 - 如果已有抽象类且新增功能只需默认实现,别急着改成接口:迁移成本(所有实现类要改
implements→extends)可能远超收益
构造器中禁止调用可被重写的方法
这是 Java 继承中最隐蔽的陷阱。父类构造器执行时,子类字段还未初始化,此时若调用被子类重写的方法,会访问到未初始化的字段,结果是 null 或 0,且无编译警告。
public class Parent {
public Parent() {
init(); // 危险!子类可能重写了 init()
}
public void init() { /* do something */ }
}
public class Child extends Parent {
private String data = "ready";
@Override
public void init() {
System.out.println(data.length()); // NullPointerException!
}
}
- 修复方式:把
init()改成private或final,或拆出静态工厂方法分两步构造 - IDEA 能检测此类问题(提示 “Call to possibly-overridden method in constructor”),但 Eclipse 默认不启用
- Spring 的
@PostConstruct就是为绕过这个限制而生——它在对象完全构造后才回调,但仅适用于受容器管理的 Bean
真正难的不是写出能跑的继承结构,而是每次加一个 extends 前,能意识到自己正在给未来埋下多少不可测的耦合点。










