(一) 问题描述
最近,同事遇到一个关于java 初始化的问题,描述如下:
子类构造器中调用super(),然后在父类构造器中调用子类有@overwrite的方法,子类在overwrite的方法中对自己成员赋值,log输出成功赋值,在子类new完,log打印发现部分成员变量值丢失了。
打印log发现list数据丢失了,int值还在。
乍看起来,很奇怪吧? 其实是因为对初始化的一些问题没有理解清楚,导致了数据丢失。初始化问题算是java中比较初级的问题,但也能看出知识点掌握的深不深、牢不牢
(二) 结论
先给出结论,关于对象实例化的顺序,简易描述如下:
先静态,再动态;
先父类,再子类;
先动态字段,再构造函数;
先静态字段,再静态块;
所以,造成上面结果的原因就是,先父类,再子类,在父类的构造函数里调用子类的read初始化后,子类又会进行初始化,list又被重新初始化了。面intVlaue为什么没有初始化?发现非静态成员,如果没有显式初始化赋值的话,相应类型的数据有默认的初始值(int类型为0,引用类型为null)。在类非静态成员初始化过程中,不会对这些值显示赋值,所以上面例子中intValue在父类构造器中赋值后,数据能够保留下来。so, 例子中的bug, 简单的解决办法还可以直接把定义地方的显示赋值去掉就ok了
如果只关注结果或结论,喜欢硬背的话,节省时间,下面可以不用看。 喜欢刨根问底的可以继续看下去,相信会理解的更好!
----------------------------------------------------------------------------------------------------
(三) 结论理解 与 分析
此节分析,上面的那段顺序为什么为是那样的顺序。让我们来分析和理解一下。
1 先静态,再动态。 静态字段是关于类的, 在 ClassLoader加载类的时候就初始化了所以静态的初始化的很早 很早
2 先父类,再子类。 是常识。 但是实现的原理,是在java编译成字节码时, 如果是默认构造函数(如 A() 或不写构造函数时 ),编译器会在编译成的字节码里 会自动加上 super(); 所以就先初始父了。 如果父类构造函数不是默认的,会编译出错,让你手动加上super(a,b)
3 先动态字段,再构造函数; 字节码角度实现上,在字节码中,会把 字段的初始化 放到字节码中的构造函数的最前面。不信来验证一下:
简单类
一个Test中的内部类CC
public static class CC {
String c1 = "ccccc";
public CC() {
c1 = "cccc2";
}
void test() {
}
}
编译后看字节码:用javap 命令可以看
Compiled from "Test.java"
public class Test$CC {
java.lang.String c1;
public Test$CC(java.lang.String);
Code:
0: aload_0
1: invokespecial #10 // Method
java/lang/Object."<init>":()V
4: aload_0
5: ldc #13 // String ccccc
7: putfield #15 // Field c1:Ljava/lang/String;
10: aload_0
11: ldc #17 // String cccc2
13: putfield #15 // Field c1:Ljava/lang/String;
16: return
void test();
Code:
0: return
}
看 1:位置 会加上 object.init吧 即自动加上super了 说明了 规律2 先父类,再子类 的问题
7: 在初始化函数里 先字段初始化了 "c1"
11: 后又进行真正初始化了 "cccc2"
所以 先动态字段,再构造函数 就很好理解了
4 先静态字段,再静态块; 这个可以根据第3条规律类推了。 从反射上来讲 Class 对象也是个Object对象, static代码块 相当于Class对象的初始化函数,static字段相当于Class对象的字段。 而按3规律的实现上讲,字节码中 可能也是 把static字段的初始化放到static块中最前面了。 此处没有验证
所以实例化过程的 终极规律 就是 如果Class对象没有加载先加载并初始化Class, 再根据Class分配一段实际对象的初始状态的内存(int 字段为 0 object 为null , bool 为false ) 然后执行对象初始化函数 这个对象就产生了 !!!! 按字节码中初始化函数自然执行,最终就是(二)中所说的顺序。