跳到主要内容

「记」子类复写父类方法与类初始化顺序引发的bug

· 阅读需 4 分钟

bug现象

bug出现的条件:

  • 继承关系
  • 子类属性有默认的初始化
  • 子类复写了父类的钩子方法
  • 钩子方法在父类构造方法中调用

可以看下以下的示例代码

public class Father {

public Father() {
init();
}

protected void init() {
}
}
public class Child extends Father {

int a = 0;

public Child() {
super();
}

@Override
protected void init() {
super.init();
a = 1;
}
}
import org.junit.Test;

import static org.junit.Assert.*;

public class ChildTest {

@Test
public void testConstructor() {
Child child = new Child();
assertEquals(0, child.a);
}
}

类加载过程

顺便复习一下类加载过程,类加载过程分为加载、链接、初始化三大步骤

加载:查找并加载类的二进制字节流数据,并且据此创建类,即代表这个类的Class对象。

链接:将创建成的类合并至Java虚拟机中,使之能够执行的过程。还分验证、准备、解析三个阶段。

  • 验证:确保被加载类的正确性
  • 准备:为类的静态变量分配内存,并将其初始化为默认值
  • 解析:把类中的符号引用转换为直接引用

初始化:标记为常量值的字段赋值,以及执行方法。初始化的时机:

  1. 虚拟机启动时,初始化用户指定的主类
  2. 当遇到以新建目标类实例的new指令时,初始化new指定的目标类
  3. 当遇到调用静态方法的指令字段是,初始化该静态方法所在的类
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类
  5. 子类的初始化会触发父类的初始化
  6. 如果一个接口定义了default方法,那么直接或间接实现该接口的类初始化,会触
  7. 该接口的初始化
  8. 使用反射API对某个类进行反射调用时,初始化这个类
  9. 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类

类初始化顺序

  1. 按定义顺序初始化父类的static成员,在准备阶段完成
  2. 按定义顺序初始化子类的static成员,在准备阶段完成
  3. 按定义顺序执行父类的普通成员初始化
  4. 执行父类的构造函数
  5. 按定义顺序执行子类的指定初始化
  6. 执行子类的构造函数

回头解bug

现在再回头看下bug代码就知道是什么原因:父类执行构造方法时调用了会修改子类成员a的钩子方法,然后子类才初始化了自己的成员,覆盖了钩子方法里对a的修改。

解决方法也简单,对a不加默认值就好了。