Java初始化顺序
首先看一段代码:
public class InitialOrderTest { // 静态变量 public static String staticField = "静态变量"; // 变量 public String field = "变量"; // 静态初始化块 static { System.out.println(staticField); System.out.println("静态初始化块"); } // 初始化块 { System.out.println(field); System.out.println("初始化块"); } // 构造器 public InitialOrderTest() { System.out.println("构造器"); } public static void main(String[] args) { new InitialOrderTest(); } }
执行结果:
这里有个构造代码块的问题,所谓的构造代码块就是直接定义没有任何修饰符、前缀、后缀的代码块即为构造代码块。我们明白一个类必须至少有一个构造函数,构造函数再生成对象时被调用。我们可以简单的记作 编译器会将代码块按照他们的顺序(假如有多个代码块)插入到所有的构造函数的最前端,这样就能保证不管调用哪个构造函数都会执行素有的构造代码块。
看下面这个例子:
public class Test { /** * 构造代码 */ { System.out.println("执行构造代码块1..."); } /** * 无参构造函数 */ public Test(){ System.out.println("执行无参构造函数..."); } /** * 构造代码 */ { System.out.println("执行构造代码块2..."); } /** * 有参构造函数 * @param id id */ public Test(String id){ System.out.println("执行有参构造函数..."); } public static void main(String[] args) { Test test = new Test(); } }
我们用反编译工具就可以看到我们想要的执行顺序
public class Test { public Test() { System.out.println("执行构造代码块1..."); System.out.println("执行构造代码块2..."); System.out.println("执行无参构造函数..."); } public Test(String id) { System.out.println("执行构造代码块1..."); System.out.println("执行构造代码块2..."); System.out.println("执行有参构造函数..."); } public static void main(String[] args) { Test test = new Test(); } }
有上面我们看出
1、 静态代码块,它是随着类的加载而被执行,只要类被加载了就会执行,而且只会加载一次,主要用于给类进行初始化。
2、 构造代码块,每创建一个对象时就会执行一次,且优先于构造函数,主要用于初始化不同对象共性的初始化内容和初始化实例环境。
3、 构造函数,每创建一个对象时就会执行一次。同时构造函数是给特定对象进行初始化,而构造代码是给所有对象进行初始化,作用区域不同。
通过上面的分析,他们三者的执行顺序应该为:静态代码块 > 构造代码块 > 构造函数。
通过JVM加载机制我们知道,类的初始化属于类加载的最后一个阶段(主要在方法区工作),会先执行<clinit>()(有静态变量和静态块组成);然后执行普通成员变量,当初始化实例时(也是对象初始化,实例初始化,相当于new在堆中创建对象),会先执行<init>(),也就是构造方法(经过编译器处理后,普通块被放到构造方法中去了)
在看一个复杂一点的例子:
class Parent { // 静态变量 public static String p_StaticField = "父类--静态变量"; // 变量 public String p_Field = "父类--变量"; // 静态初始化块 static { System.out.println(p_StaticField); System.out.println("父类--静态初始化块"); } // 初始化块 { System.out.println(p_Field); System.out.println("父类--初始化块"); } // 构造器 public Parent() { System.out.println("父类--构造器"); } }//如果你想把这两个类放在同一个文件且名字起为SubClass, 父类前不能加public public class SubClass extends Parent { // 静态变量 public static String s_StaticField = "子类--静态变量"; // 变量 public String s_Field = "子类--变量"; // 静态初始化块 static { System.out.println(s_StaticField); System.out.println("子类--静态初始化块"); } // 初始化块 { System.out.println(s_Field); System.out.println("子类--初始化块"); } // 构造器 public SubClass() { System.out.println("子类--构造器"); } // 程序入口 public static void main(String[] args) { new SubClass(); } }
执行结果:
先父类的<clinit>(),后子类的<clinit>();先父类的<init>(),后子类的<init>();
解释一下 <clinit>包含的东西:
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,在前面的静态语句块可以赋值,但是不能访问。
<clinit>()方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。
<clinit>()方法对于类或者接口来说不是必须的,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。
下面看一道有趣的题目:
public class Test { public static Test t=new Test(); public static int a=0; public static int b=1; private Test(){ a++; b++; System.out.println("构造方法中:a="+a+" b="+b); } public static Test getInstance(){ return t; } public static void main(String[] args){ Test tt=Test.getInstance(); System.out.println("main方法中:tt.a="+tt.a+" tt.b="+tt.b); } }
这里出现了一个现象, 首先在main 方法中找到 需要初始化类 test,然后发现这个类是本类的实例,这时候就要在还没有静态初始化结束就要开始了 初始化实例部分了。
类初始化,先执行<clinit>(),而<clinit>()中有public static T t=new T();public static int a=0;public static int b=1。先执行第一句public static T t=new T(),执行这句,那就相当于new一个对象(实例化对象),它会先执行<init>(),接着执行private T()方法了,但是此时a=0,b=0;所以a++,b++后,a=1,b=1。和运行结果一致。然后再回到<clinit>()中执行public static int a=0(这个0就是程序上写的0),public static int b=1;这个时候a=0,b=1了,而不再是a=1,b=1了。