天天看點

深入剖析java類的構造方式

概要:本文通過檢視一個精心構造的類結構的運作輸出和使用javap工具檢視實際生成的java位元組碼(bytecode)向java程式員展示了一個類在運作時是如何構造生成的。

    關鍵字: java 構造 javap 位元組碼 bytecode

    按照java規範,一個類執行個體的構造過程是遵循以下順序的:

1.    如果構造方法(constructor,也有翻譯為構造器和構造函數的)是有參數的則進行參數綁定。

2.    記憶體配置設定将非靜态成員賦予初始值(原始類型的成員的值為規定值,例如int型為0,float型為0.0f,boolean型為false;對象類型的初始值為null),靜态成員是屬于類對象而非類執行個體,是以類執行個體的生成不進行靜态成員的構造或者初始化,後面将講述靜态成員的生成時間。

3.    如果構造方法中存在this()調用(可以是其它帶參數的this()調用)則執行之,執行完畢後進入第6步繼續執行,如果沒有this調用則進行下一步。

4.    執行顯式的super()調用(可以是其它帶參數的super()調用)或者隐式的super()調用(預設構造方法),此步驟又進入一個父類的構造過程并一直上推至Object對象的構造。

5.    執行類申明中的成員指派和初始化塊。

6.    執行構造方法中的其它語句。

現在來看看精心構造的一個執行個體:

<b>class</b> Parent

{

  <b>int</b> pm1;

  <b>int</b> pm2=10;

  <b>int</b> pm3=pmethod();

  {

    <b>System</b>.out.println("Parent's instance initialize block");  

  } 

  <b>public</b> <b>static</b> <b>int</b> spm1=10;

  <b>static</b>

    <b>System</b>.out.println("Parent's static initialize block");

  }

  Parent()

    <b>System</b>.out.println("Parent's default constructor");

  <b>static</b> <b>void</b> staticmethod()

    <b>System</b>.out.println("Parent's staticmethod");

  <b>int</b> pmethod()

    <b>System</b>.out.println("Parent's method");

    <b>return</b> 3;

}

<b>class</b> Child <b>extends</b> Parent

  <b>int</b> cm1;

  <b>int</b> cm2=10;

  <b>int</b> cm3=cmethod();

  Other co;

  <b>public</b> <b>static</b> <b>int</b> scm1=10;

    <b>System</b>.out.println("Child's instance initialize block");  

    <b>System</b>.out.println("Child's static initialize block");

  Child()

      co=<b>new</b> Other();

    <b>System</b>.out.println("Child's default constructor");

  Child(<b>int</b> m)

      <b>this</b>();

      cm1=m;

    <b>System</b>.out.println("Child's self-define constructor");

    <b>System</b>.out.println("Child's staticmethod");

  <b>int</b> cmethod()

    <b>System</b>.out.println("Child's method");

<b>class</b> Other

    <b>int</b> om1;

    Other() {

    <b>System</b>.out.println("Other's default constructor");

    }

<b>public</b> <b>class</b> InitializationTest

  <b>public</b> <b>static</b> <b>void</b> main(<b>String</b> args[])

    Child c;

    <b>System</b>.out.println("program start");

    <b>System</b>.out.println(Child.scm1);

    c= <b>new</b> Child(10);

    <b>System</b>.out.println("program end");

 }

進入此檔案所在的目錄,然後

編譯此檔案:javac InitializationTest.java

運作此程式:java ?classpath . InitializationTest

得到的結果是:

program start

Parent's static initialize block

Child's static initialize block

10

Parent's method

Parent's instance initialize block

Parent's default constructor

Child's method

Child's instance initialize block

Other's default constructor

Child's default constructor

Child's self-define constructor

program end

如果沒有看過上面的關于類的構造的說明,很容易讓人誤解為類的構造順序是如下的結果(忽略參數綁定、記憶體配置設定和非靜态成員的預設值指派):

1.    完成父類的非靜态成員初始化指派以及執行初始化塊(這個的先後順序取決于源檔案中的書寫順序,可以将初始化塊置于成員聲明前,那麼先執行的将是初始化塊,将上面的代碼稍稍變動一下就可以驗證這一點。)

2.    調用父類的構造方法完成父類構造。

3.    完成非靜态成員的初始化指派以及執行初始化塊。

4.    調用構造方法完成對象的構造,執行構造方法體中的其它内容。

如果根據以上java規範中給出的順序也可以合理的解釋程式的輸出結果,那麼如何親眼看到是規範中的順序而不是以上根據程式的輸出推斷的順序呢?

下面就使用JDK自帶的javap工具看看實際的順序,這個工具是一個根據編譯後的位元組碼生成一份位元組碼的助記符格式的文檔的工具,就像根據機器碼生成彙編代碼那樣。

反編譯:javap -c -classpath . Child

輸出的結果是(已經經過标記,交替使用黑體和斜體表示要講解的每一塊):

Compiled from InitializationTest.java

class Child extends Parent {

    int cm1;

    int cm2;

    int cm3;

    Other co;

    public static int scm1;

    static {};

    Child();

    Child(int);

    int cmethod();

    static void staticmethod();

Method static {}

   0 bipush 10

   2 putstatic #22 &lt;Field int scm1&gt;

   5 getstatic #20 &lt;Field java.io.PrintStream out&gt;

   8 ldc #5 &lt;String "Child's static initialize block"&gt;

  10 invokevirtual #21 &lt;Method void println(java.lang.String)&gt;

  13 return

Method Child()

   0 aload_0

   1 invokespecial #14 &lt;Method Parent()&gt;

   4 aload_0

   5 bipush 10

   7 putfield #16 &lt;Field int cm2&gt;

  10 aload_0

  11 aload_0

  12 invokevirtual #18 &lt;Method int cmethod()&gt;

  15 putfield #17 &lt;Field int cm3&gt;

  18 getstatic #20 &lt;Field java.io.PrintStream out&gt;

  21 ldc #2 &lt;String "Child's instance initialize block"&gt;

  23 invokevirtual #21 &lt;Method void println(java.lang.String)&gt;

  26 aload_0

  27 new #8 &lt;Class Other&gt;

  30 dup

  31 invokespecial #13 &lt;Method Other()&gt;

  34 putfield #19 &lt;Field Other co&gt;

  37 getstatic #20 &lt;Field java.io.PrintStream out&gt;

  40 ldc #1 &lt;String "Child's default constructor"&gt;

  42 invokevirtual #21 &lt;Method void println(java.lang.String)&gt;

  45 return

Method Child(int)

   1 invokespecial #12 &lt;Method Child()&gt;

   5 iload_1

   6 putfield #15 &lt;Field int cm1&gt;

   9 getstatic #20 &lt;Field java.io.PrintStream out&gt;

  12 ldc #4 &lt;String "Child's self-define constructor"&gt;

  14 invokevirtual #21 &lt;Method void println(java.lang.String)&gt;

  17 return

Method int cmethod()

   0 getstatic #20 &lt;Field java.io.PrintStream out&gt;

   3 ldc #3 &lt;String "Child's method"&gt;

   5 invokevirtual #21 &lt;Method void println(java.lang.String)&gt;

   8 iconst_3

   9 ireturn

Method void staticmethod()

   3 ldc #6 &lt;String "Child's staticmethod"&gt;

   8 return

請仔細浏覽一下這個輸出并和源代碼比較一下。

下面解釋如何根據這個輸出得到類執行個體的實際的構造順序,在開始說明前先解釋一下輸出的語句的格式,語句中最前面的一個數字是指令的偏移值,這個我們在此可以不管,第二項是指令助記符,可以從字面上大緻看出指令的意思,例如 getstatic 指令将一個靜态成員壓入一個稱為操作數堆棧(後續的指令就可以引用這個資料結構中的成員)的資料結構,而 invokevirtual 指令是調用java虛拟機方法,第三項是操作數(#号後面跟一個數字,實際上是類的成員的标記),有些指令沒有這一項,因為有些指令如同彙編指令中的某些指令一樣是不需要操作數的(可能是操作數是隐含的或者根本就不需要),這是java中的一個特色,如果你直接檢查位元組碼,你會看到成員資訊沒有直接嵌入指令而是像所有由java類使用的常量那樣存儲在一個共享池中,将成員資訊存儲在一個常量池中可以減小位元組碼指令的大小,因為指令隻需要存儲常量池中的一個索引而不是整個常量,需要說明的是常量池中的項目的順序是和編譯器相關的,是以在你的環境中看到的可能和我上面給出的輸出不完全一樣,第四項是對前面的操作數的說明,實際的位元組碼中也是沒有的,根據這個你能很清楚的得到實際上使用的是哪個成員或者調用的是哪個方法,這也是javap為我們提供的便利。說完上面這些你現在應該很容易看懂上面的結果和下面将要叙述的内容了。其它更進一步的有關java位元組碼的資訊請自己查找資料。

先看看最開始的部分,很像一個标準的c++類的聲明,确實如此。成員聲明的後面沒有了成員初始化指派語句和初始化塊,那麼這些語句何時執行的呢?先不要急,繼續往下看。

第二塊,是一個Method static {},對比看看第一部分,它被處理為一個靜态的方法(從前面的Method可以看出),這就是源代碼中的靜态初始化塊,從後面的語句可以看出它執行的就是System.out.println("Child's static initialize block")語句,由于這個方法是沒有方法名的,是以它不能被顯式的調用,它在何處調用後面會有叙述。

第三塊,預設構造方法的實作,這是本文的重點,下面詳細講解。由于源代碼中的預設構造方法沒有顯式調用this方法,是以沒有this調用(對比看看下一塊的有參的構造方法的前兩句),同時也沒有顯式的super調用,那麼隐式調用父類的預設構造方法,也就是前兩條語句(主要是語句invokespecial #14 &lt;Method Parent()&gt;),它調用父類的構造方法,和這個類的構造相似(你可以使用javap ?c ?classpath . Parent反編譯父類的位元組碼看看這個類的構造過程);緊接着的是執行源代碼中的第一條初始化指派語句cm2=10(即接下來的三條語句,主要是bipush 10和putfield #15 &lt;Field int cm2&gt;,此處回答了第一塊中的疑問,即初始化指派語句到哪兒去了。);接下來是執行cm3=cmethod()(接下來的四條語句);然後是執行初始化塊中的内容System.out.println("Child's instance initialize block")(接下來的三條語句);java規範内部約定的内容至此執行完畢,開始執行構造方法的方法體中的内容,即co=new Other()(接下來的五條語句)和System.out.println("Child's default constructor")(接下來的三條語句),最後方法執行完畢傳回(最後一條語句return)。

剩下的幾塊相信應該不用解釋了吧,有參構造方法調用無參構造方法然後執行自己的方法體,成員方法cmethod執行一條列印語句然後傳回一個常量3,靜态方法staticmethod執行一條列印語句。

另外需要說明一下的是你可以将有參構造方法中的this調用去掉,然後看看反編譯的結果,你會發現兩個構造方法非常的類似,如果你将兩個構造方法的内容改為一樣的,那麼反編譯後的生成也将是同樣的。從這個可以說明本文開始的構造順序的說明中構造方法中this調用的判斷是在編譯階段就完成的,而不是在運作階段(說明中的意思好像是這個判斷是在運作時進行的)。

對構造過程的另一個細節你可能還不相信,就是順序中的第二條關于非靜态成員的賦予預設初始值(記憶體配置設定部分無法考證,這是java虛拟機自動完成的),這個你可以通過在子類Child的cmethod方法的最開始用 System.out.println(cm3)列印cm3的值(輸出為0,其它類型成員的值可以通過類似的方法得到)。

下面來講解另一個還沒有解決的問題:靜态成員初始化和靜态初始化塊的執行是在何時完成的?這個可以通過一個小小的試驗推斷得到:是在第一次使用該類對象時進行的(注意是類對象而不是類執行個體,對于類的公有靜态成員可以直接通過類名進行通路,并不需要生成一個類執行個體,這就是一次類對象的使用而非類執行個體的使用,如果在生成第一個類執行個體前沒有使用過該類對象,那麼在構造第一個類執行個體前先完成類對象的構造(即完成靜态成員初始化以及執行靜态初始化塊),然後再執行以上類執行個體的構造過程),試驗的步驟如下:

1.    修改main方法,将其中的System.out.println(Child.scm1)和c= new Child(10)都注釋掉(不要删除,後面還需要用到這兩個語句),編譯運作程式,輸出将隻有program start和program end,這說明沒有使用類對象也沒有生成類執行個體時不進行靜态成員的構造。

2.    将System.out.println(Child.scm1)的注釋取消,編譯運作後輸出多了父類和子類的靜态初始化塊部分的執行輸出(使用子類的類對象将導緻生成父類的類對象,父類先于子類構造)。

3.    将System.out.println(Child.scm1)注釋掉并取消c= new Child(10)的注釋,編譯運作後輸出隻比最開始沒有注釋任何語句時少了一條(輸出Child.scm1的值10)

下一篇: __init和__exit