子类不该覆盖父类的public方法,除非明确设计为可重写;应优先用组合代替继承,避免构造器中调用可重写方法,并根据是否需共享状态选择接口或抽象类。
Java 中 public 方法默认是“开放扩展点”,
但多数业务类并非为继承而设计。如果父类没用 @Override 注解标记、也没在 Javadoc 里写明“此方法可被子类重写”,那它大概率不是契约的一部分。
常见错误现象:NullPointerException 出现在子类重写的 toString() 里,只因父类构造器中调用了该方法(此时子类字段尚未初始化);或重写 hashCode() 却没同步改 equals(),导致 HashSet 行为异常。
final,或是否在构造器中调用该方法protected,或加 @Override + 完整 Javadoc 说明契约public 方法,但 javac 不校验语义合理性比如 Employee 类继承 Person 看似合理,但一旦业务要求“兼职学生员工”同时有 Student 和 Employee 属性,单继承立刻失效。这时 Employee 应持有 Person 实例,而非继承它。
使用场景:需要复用逻辑但又不想暴露父类接口、需运行时切换行为(如策略模式)、或父类来自第三方 SDK(无法修改)。
private final Person person;,而非 extends Person
getName() { return person.getName(); })换来的是长期可维护性Java 8+ 允许接口含 default 方法,但接口仍不能定义实例字段。如果多个子类必须共用一个 private int retryCount; 或缓存 Map,抽象类更合适;否则一律用接口。
参数差异:interface 支持多实现,abstract class 只能单继承;接口方法默认 public abstract,抽象类方法可设 protected 或包私有。
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,或拆出静态工厂方法分两步构造@PostConstruct 就是为绕过这个限制而生——它在对象完全构造后才回调,但仅适用于受容器管理的 Bean真正难的不是写出能跑的继承结构,而是每次加一个 extends 前,能意识到自己正在给未来埋下多少不可测的耦合点。