在学习编码规范时,明确要求在构造方法内不应调用可被覆写的方法。学习了几篇文章后,发现这里面涉及类的初始化流程,还是挺复杂的,怕自己只是貌似明白了,所以还是尝试用自己的话写一写,指不定哪天就被问到呢,先记下来。
类的初始化顺序
一般类加载过程
通过跑完下面代码,我们可以看到一个类的加载过程为:
静态变量/静态代码块 -> main方法 -> 非静态变量/代码块 -> 构造方法
public class SimpleClassLoadTest {
public Test testDynamic = new Test("Dynamic");
public static Test testStatic = new Test("Static");
public static void main(String[] args) {
System.out.println("Run into mian");
new SimpleClassLoadTest();
}
{
System.out.println("Code block");
}
static {
System.out.println("Static code block");
}
public static Test testStaticSecond = new Test("Static Second");
public SimpleClassLoadTest() {
System.out.println("Constructor SimpleClassLoadTest");
}
}
class Test {
public Test(String flag) {
System.out.println("Constructor Test flag: " + flag);
}
}
// 输出:
Constructor Test flag: Static
Static code block
Constructor Test flag: Static Second
Run into mian
Constructor Test flag: Dynamic
Code block
Constructor SimpleClassLoadTest
如输出所示,其中静态变量和静态代码块先后顺序决定了他们的执行顺序,所以限制性flag为Static的Test初始化,再执行静态块代码,最后执行flag为Second的Test初始化。
有继承关系的类加载过程
通过跑完下面代码,我们可以看到有继承关系的类的加载过程为:
父类静态变量/静态代码块
子类静态变量/静态代码块
父类变量/代码块
父类构造器
子类变量/代码块
子类构造器
public class ExtendClassLoadTest {
public ExtendClassLoadTest() {
System.out.println("Constructor - ExtendClassLoadTest");
new Son();
}
public static void main(String[] args) {
new ExtendClassLoadTest();
}
}
class Father {
public String dynmaicStr = "Father - dynmaicStr";
public static String staticStr = "Father - staticStr";
public int value;
{
System.out.println("value = " + value + " " + dynmaicStr);
}
static {
System.out.println(staticStr);
}
public Father() {
System.out.println("constructor Father value = " + value);
add();
System.out.println("constructor Father after add value = " + value);
}
protected void add() {
value += 12;
}
}
class Son extends Father {
public String dynmaicStr = "Son - dynmaicStr";
public static String staticStr = "Son - staticStr";
{
System.out.println("value = " + value + " " + dynmaicStr);
}
static {
System.out.println(staticStr);
}
@Override
protected void add() {
value += 44;
}
public Son() {
System.out.println("constructor Son value = " + value);
add();
System.out.println("constructor Son after add value = " + value);
}
}
// 输出:
Constructor - ExtendClassLoadTest
Father - staticStr
Son - staticStr
value = 0 Father - dynmaicStr
constructor Father value = 0
constructor Father after add value = 44
value = 44 Son - dynmaicStr
constructor Son value = 44
constructor Son after add value = 88
上述输出不难看出,父子之间先父类再子类,动静之间静态后动态,变量/代码块在构造器前。
细心的同学估计已经发现了子类构造方法中打印的value值有些不合理,这也是本篇文章所要讨论的关键所在。
测试代码
当父类方法被子类覆写后,用父类在构造方法中实际调用的是子类的覆写方法。当父类构造器中调用add()时他实际调用的是子类的add(),所以会打出constructor Father after add value = 44
。我们知道对于成员变量,类在初始化时都会有初始化值,8个基本类型分别是0,0.0,false,其他引用类型是null,如果add()调用的成员变量是Integer的,则会有空指针异常抛出导致程序崩溃,而这个问题在编译时不会暴露出来,这也就是为什么题目中写道“在构造方法内不应调用可被覆写的方法”。
根因就是父类的构造器在子类构造器之前执行,但父类调用的成员方法优先使用子类的覆写方法。
public class NoUseOverrideMethodInFatherConstructorTest {
public static void main(String[] args) {
Base base = new Base();
base.printValue();
Base sub = new Sub();
sub.printValue();
}
}
class Base {
protected Integer value;
protected void show() {
System.out.println("Base show");
}
protected void add() {
}
public Base() {
show();
add();
}
public void printValue() {
System.out.println("Base value = " + value);
}
}
class Sub extends Base {
public Sub() {
value = new Integer(12);
}
@Override
protected void add() {
value++;
}
@Override
public void printValue() {
System.out.println("Sub value = " + value);
}
}
测试代码路径:
thinkinginjava/chapter07reusingclasses
参考:
JAVA类初始化顺序总结