在java中初始化时非常重要的一个问题,借着之前的文章对初始化的顺序进行一个总结:
初始化顺序图:
无论是继承还是组合的关系都是这个顺序
通过下面两个代码验证:
class Father{
static String father = "Father的静态变量";
static int i =1;
static{
System.out.println(father);
System.out.println("Father的静态初始化块");
}
int j = 9;
{
System.out.println("Father的初始化块");
System.out.println("Father的非静态属性j = "+j);
}
Father(){
System.out.println("Father的构造方法");
System.out.println("Father构造方法中输出i="+i+",j="+j);
}
}
class Child extends Father{
static String child = "child的静态变量";
static int k = 2;
static {
System.out.println(child);
System.out.println("child的静态初始化块");
}
int l = 10;
{
System.out.println("child的初始化块");
System.out.println("child的非静态属性l="+l);
}
Child(){
System.out.println("child的构造方法");
System.out.println("Child构造方法中输出k="+k+",l="+l);
}
}
public class Demo6 {
public static void main(String[] args) {
Child child = new Child();
}
}
class Father1{
static String father = "Father的静态变量";
static int i =1;
static{
System.out.println(father);
System.out.println("Father的静态初始化块");
}
int j = 9;
{
System.out.println("Father的初始化块");
System.out.println("Father的非静态属性j = "+j);
}
Father1(){
System.out.println("Father的构造方法");
System.out.println("Father构造方法中输出i="+i+",j="+j);
}
}
class Child1 {
Father1 father1 = new Father1();
static String child = "child的静态变量";
static int k = 2;
static {
System.out.println(child);
System.out.println("child的静态初始化块");
}
int l = 10;
{
System.out.println("child的初始化块");
System.out.println("child的非静态属性l="+l);
}
Child1(){
System.out.println("child的构造方法");
System.out.println("Child构造方法中输出k="+k+",l="+l);
}
}
public class Demo7 {
public static void main(String[] args) {
Child child = new Child();
}
}
两个的输出结果相同:
Father的静态变量
Father的静态初始化块
child的静态变量
child的静态初始化块
Father的初始化块
Father的非静态属性j = 9
Father的构造方法
Father构造方法中输出i=1,j=9
child的初始化块
child的非静态属性l=10
child的构造方法
Child构造方法中输出k=2,l=10
经过上面的两个代码的测试,我们就已经可以得出图片所给的结论了。
总而言之,实例化一个类的对象,是一个典型的递归过程,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。
但是,在执行构造器之前,对象中的所有基本类型都会被设定为默认值,对象引用设为null——这是通过将对象内存设为二进制零值而一举生成的。然后才会调用构造器。
在构造器内部的多态方法的行为可以更清楚的看到类加载将对象内存设为二进制零值
代码如下:
class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph.RoundGlyph(),radius = "+radius);
}
void draw(){
System.out.println("RoundGlyph.draw(),radius = "+radius);
}
}
class RectangularGlyph extends Glyph{
private int radius = 1;
RectangularGlyph(int r){
radius = r;
System.out.println("RectangularGlyph.RectangularGlyph(),radius = "+radius);
}
void draw(){
System.out.println("RectangularGlyph.draw(),radius = "+radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
new RectangularGlyph(5);
}
}
输出结果:
Glyph() before draw()
RoundGlyph.draw(),radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius = 5
Glyph() before draw()
RectangularGlyph.draw(),radius = 0
Glyph() after draw()
RectangularGlyph.RectangularGlyph(),radius = 5
Glyph.draw方法会被RoundGlyph.draw() 方法覆盖,但是在Glyph的构造器调用了这个方法,导致了对RoundGlyoh.draw()方法的调用,但是因为还没有执行子类的radius=1的语句就调用了子类的draw()方法,所以输出的radius的值为0,也就说明了在任何事物发生之前,会先将分配给对象的存储空间初始化成二进制的零。
在java编程思想中给出的初始化的实际过程是:
1.在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
2调用基类的构造器。这个步骤会不断的反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最低层的导出类。
3.按声明的顺序调用成员的初始化方法。
4.调用导出类的构造器主体。
Java类的初始化时机
什么情况下虚拟机需要开始初始化一个类呢?这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化(而这一过程自然发生在加载、验证、准备之后):
1) 遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化)时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
使用new关键字实例化对象的时候;
读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;
调用一个类的静态方法的时候。
2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5) 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
注意,对于这五种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这五种场景中的行为称为对一个类进行 主动引用。除此之外,所有引用类的方式,都不会触发初始化,称为 被动引用。
特别需要指出的是,类的实例化与类的初始化是两个完全不同的概念:
类的实例化是指创建一个类的实例(对象)的过程;
类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。
被动引用
1)、通过子类引用父类的静态字段,不会导致子类初始化
2)、通过数组定义来引用类,不会触发此类的初始化
3)、常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化