JAVA類初始化及例項初始化時內部的執行順序
記得剛畢業時,應聘JAVA開發崗位,做招聘單位的筆試時,經常有JAVA類內部的執行順序的考察,就是讓你寫出某個程式的列印結果的順序,現在整理一下。
如果一個類,有構造器,普通塊,靜態塊,那該類初始化時,它的執行順序如何呢?如果它有父類,並且它的父類也有構造器,普通塊,靜態塊呢?直接寫個小程式,測一下,就一目瞭然。
public class A { public A(){ System.out.println("我是構造器"); } { System.out.println("我是普通塊"); } static{ System.out.println("我是靜態塊"); } public static void main(String[] args){ new A(); } }
執行結果是:
我是靜態塊
我是普通塊
我是構造器
順序是如何的,就不用我多說了,一目瞭然。那麼下面來看看,它編譯後的樣子,是不是順序也是這樣的。
Compiled from "A.java" public class test.A extends java.lang.Object SourceFile: "A.java" minor version: 0 major version: 50 Constant pool: const #1 = class #2; // test/A const #2 = Asciz test/A; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz <clinit>; const #6 = Asciz ()V; const #7 = Asciz Code; const #8 = Field #9.#11; // java/lang/System.out:Ljava/io/PrintStream; const #9 = class #10; // java/lang/System const #10 = Asciz java/lang/System; const #11 = NameAndType #12:#13;// out:Ljava/io/PrintStream; const #12 = Asciz out; const #13 = Asciz Ljava/io/PrintStream;; const #14 = String #15; // 我是靜態塊 const #15 = Asciz 我是靜態塊; const #16 = Method #17.#19; // java/io/PrintStream.println:(Ljava/lang/String;)V const #17 = class #18; // java/io/PrintStream const #18 = Asciz java/io/PrintStream; const #19 = NameAndType #20:#21;// println:(Ljava/lang/String;)V const #20 = Asciz println; const #21 = Asciz (Ljava/lang/String;)V; const #22 = Asciz LineNumberTable; const #23 = Asciz LocalVariableTable; const #24 = Asciz <init>; const #25 = Method #3.#26; // java/lang/Object."<init>":()V const #26 = NameAndType #24:#6;// "<init>":()V const #27 = String #28; // 我是普通塊 const #28 = Asciz 我是普通塊; const #29 = String #30; // 我是構造器 const #30 = Asciz 我是構造器; const #31 = Asciz this; const #32 = Asciz Ltest/A;; const #33 = Asciz main; const #34 = Asciz ([Ljava/lang/String;)V; const #35 = Method #1.#26; // test/A."<init>":()V const #36 = Asciz args; const #37 = Asciz [Ljava/lang/String;; const #38 = Asciz SourceFile; const #39 = Asciz A.java; { static {}; Code: Stack=2, Locals=0, Args_size=0 0: getstatic #8; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #14; //String 我是靜態塊 5: invokevirtual #16; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 12: 0 line 3: 8 public test.A(); Code: Stack=2, Locals=1, Args_size=1 0: aload_0 1: invokespecial #25; //Method java/lang/Object."<init>":()V 4: getstatic #8; //Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #27; //String 我是普通塊 9: invokevirtual #16; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: getstatic #8; //Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #29; //String 我是構造器 17: invokevirtual #16; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: return LineNumberTable: line 5: 0 line 9: 4 line 6: 12 line 7: 20 LocalVariableTable: Start Length Slot Name Signature 0 21 0 this Ltest/A; public static void main(java.lang.String[]); Code: Stack=1, Locals=1, Args_size=1 0: new #1; //class test/A 3: invokespecial #35; //Method "<init>":()V 6: return LineNumberTable: line 16: 0 line 17: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 args [Ljava/lang/String; }
從第1行到45行,或者從47行到99行,都可以看出執行順序。
從第1行到45行,可以看到,static{}塊在JVM中會生成一個叫<clinit>的方法(第11行),普通塊和構造方法合在一起會生成一個叫<init>的方法(第30行)。
總的來說:類初始化屬於類載入的最有一個階段(主要在方法區工作),會先執行<clinit>()(有靜態變數和靜態塊組成,詳細說明看後文);然後執行普通成員變數,當初始化例項時(也是物件初始化,例項初始化,相當於用new在堆中建立物件),會先執行<init>(),也就是構造方法(經過編譯器處理後,普通塊被放到構造方法中去了)。
普通塊是不是被編譯期嵌入到構造器中去了呢?下面寫個程式對比一下,編譯前和編譯後代碼的樣子就知道了。
編譯前的程式碼
public class B {
public static String s="abc";
static{
System.out.println("我是靜態塊");
}
public int a=1;
public B(){
System.out.println("我是構造器"+a);//列印後,發現a=1,而不是載入時a為預設值0,
// 說明普通成員的優先於構造方法執行
}
{
System.out.println("我是普通塊");
}
public B(String ss){
int c=2;
System.out.println("我是構造器"+c);
}
public static void main(String[] args){
new B();
new B("");
}
}
編譯後用反編譯軟體看到的程式碼
public class B
{
public static String s = "abc";
public int a = 1;
static
{
System.out.println("我是靜態塊");
}
public B()
{
System.out.println("我是普通塊");
System.out.println("我是構造器" + this.a);
}
public B(String ss) {
System.out.println("我是普通塊");
int c = 2;
System.out.println("我是構造器" + c);
}
public static void main(String[] args) {
new B();
new B("");
}
}
通過對比可看出,普通塊都被編譯期放到了所有的構造方法中,而且是方法體裡面第一行。
那麼如果父類中也有靜態塊,普通塊,構造器呢?先看看程式。
public class P {
public P(){
System.out.println("我是父類構造器");
}
{
System.out.println("我是父類普通塊");
}
static{
System.out.println("我是父類靜態塊");
}
}
public class A extends P{
public A(){
System.out.println("我是子類構造器");
}
{
System.out.println("我是子類普通塊");
}
static{
System.out.println("我是子類靜態塊");
}
public static void main(String[] args){
new A();
}
}
執行結果是:
我是父類靜態塊
我是子類靜態塊
我是父類普通塊
我是父類構造器
我是子類普通塊
我是子類構造器
由於靜態塊是在類初始化的時候就執行了,所以最先輸出,這點沒問題。再看執行結果可知,JVM是先載入父類然後再載入子類,也就是說先執行父類的<clinit> 再執行子類的<clinit>;接著是物件初始化了,由執行結果可知,也是先例項初始化父類,然後再例項初始化子類。也許有人問,我沒有new父類啊,父類怎麼也例項初始化了?是因為當你new一個子類時,JVM會先檢查子類是否有父類,有的話則先new父類。上面那題,執行new A()時,檢查到有父類P,就先new P(),但是又檢查到P有父類Object,所以又先new Object();最後的順序是new Object()->new P()->new A();其實類初始化和例項初始化過程也是一樣的,先父類,後子類。
一句話:先父類的<clinit>(),後子類的<clinit>();先父類的<init>(),後子類的<init>()
上面提到了<clinit()>,那它包含什麼東西呢?
可知,<clinit>()方法包含了類中所有的類變數(用static修飾的變數,方法中的區域性變數是不能用static修飾的,除非是final static這樣修飾)和靜態語句塊(static{}塊),它們在<clinit>()中的順序和程式中的寫的順序一樣。靜態語句塊中只能訪問到定義在它之前的變數(類變數),不能訪問定義在它之後的類變數,但可以賦值。在eclipse下寫點程式碼驗證了一下,發現編譯時就提示錯誤了,呵呵。 |
上面說了<clinit>()中的順序和程式中的順序一樣的,如果靜態變數在靜態塊前,會先執行靜態變數。下面看個例子,直觀點。
我是A類靜態塊 這裡沒使用基礎,但是卻先執行了A,可見上面的說法沒錯。 來個初始化順序圖 |
下面來看一題有趣的題 |
構造方法中:a=1 b=1 這就要說說類載入和類初始化,例項化賦值了。 1.JVM要執行main方法時,要先載入T類檔案(class檔案),這時的變數都有一個預設值。如程式中t=null,a=0(這個0是預設的0,不是程式上寫的0),b=0 2.類初始化,先執行<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了。 3.初始化結束了,然後執行到main方法中的T tt=T.getInstance(),這時tt指向的物件中的a=0,b=1 那麼如果我把程式中的變數順序調一下呢
構造方法中:a=1 b=2 |
上面的a,b都用了static修飾,那如果去掉static呢?
構造方法中:a=1 b=2 |
再調一下public static T t=new T();的位置
構造方法中:a=1 b=2 |
發現結果都一致,為什麼和a,b都加了static那2題不一樣了呢?下面來說說a,b都不加 static修飾的這2題的執行順序. 1.載入class檔案時,成員變數(不管是類變數還是例項變數)都先給一個預設值,此時t=null,a=0,b=0 2.類初始化.<clinit>()中只有public static T t=new T();所以執行new T(),而執行new T()就是建立物件(例項化物件),此時接著執行<init>(),會先執行普通成員變數a=0,b=1,所以執行到這,a=0,b=1了。接著執行T()方法,a++,b++,這是a=1,b=2了。所以構造方法中列印a=1,b=2;然後,再返回<clinit>(),但是此時<clinit>()沒有其他的了,所以a=1,b=2就保持了下來。 |