原文:https://blog.saymagic.cn/2017/07/01/class-common-question.html
我们尝试从class文件中找到答案。来看这样的一段代码:
public class InitialOrderTest { public static String staticField = " StaticField"; public String fieldFromMethod = getStrFromMethod(); public String fieldFromInit = " InitField"; static { System.out.println( "Call Init Static Code" ); System.out.println( staticField ); } { System.out.println( "Call Init Block Code" ); System.out.println( fieldFromInit ); System.out.println( fieldFromMethod ); } public InitialOrderTest() { System.out.println( "Call Constructor" ); } public String getStrFromMethod(){ System.out.println("Call getStrFromMethod Method"); return " MethodField" ; } public static void main( String[] args ) { new InitialOrderTest(); } }
结果:
我们来一一来看一下它的class文件中的内容,首先是有一个static方法区:
static {}; descriptor: ()V flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: ldc #14 // String StaticField 2: putstatic #15 // Field staticField:Ljava/lang/String; 5: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #16 // String Call Init Static Code 10: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 16: getstatic #15 // Field staticField:Ljava/lang/String; 19: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 22: return
Java编译器在编译阶段会将所有static的代码块收集到一起,形成一个特殊的方法,这个方法的名字叫做clinit
, 这个名字容易让我们联想到构造函数的名称叫做init
,但与构造函数不同,这个方法在Java层中是调用不到的,并且,这个函数是在这个类被加载时,由虚拟机进行调用。注意的是,是类被加载,而不是类被初始化成实例。所以,静态代码块的加载优先于普通的代码块,也优先于构造函数。这属于虚拟机规定的范畴,我们不做更深入的探讨。
在Class文件中,是没有为普通方法区开辟类似于clinit
这种方法的,而是将所有普通方法区的代码都合并到了构造函数中,我们直接来看构造函数:
public InitialOrderTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: aload_0 6: invokevirtual #2 // Method getStr:()Ljava/lang/String; 9: putfield #3 // Field field:Ljava/lang/String; 12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 15: aload_0 16: getfield #3 // Field field:Ljava/lang/String; 19: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 22: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 25: ldc #6 // String Init Block 27: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 30: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 33: ldc #7 // String Constructor 35: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 38: return
通过分析构造函数,我们就可以对一个实例初始化的顺序一清二楚,首先,0,1在构造函数中调用了父类的构造函数,接着,4、5、6、9为成员变量进行赋值,25、27在执行实例的代码块,最后,33、35才是执行我们Java文件中编写的构造函数的代码。这样,一个普通类的初始化顺序大致如下:
静态代码按照顺序初始化 -> 父类构造函数 -> 变量初始化 -> 实例代码块 -> 自身构造函数