繼承關系的類初始化和實例化的順序
就像之前的一個評論.我們學習的是思路. 很多人都知道繼承關系的類的初始化和實例化的順序,但如果忘記了怎么辦? 如何找到自己的答案? 又如果遇到的問題是關于泛型的擦除問題,又該如何去分析?
思路,重點是思路.泛型擦除先不談.看繼承. 首先給出一個例子,看看它的輸出是什么.
- public class A {
- private static String a = "NA";
- private String i="NA";
- {
- i = "A";
- System.out.println(i);
- }
- static {
- a = "Static A";
- System.out.println(a);
- }
- public A() {
- System.out.println("Construct A");
- }
- }
- public class B extends A {
- private static String b = "NB";
- private String j="NB";
- {
- j = "B";
- System.out.println(j);
- }
- static {
- b = "Static B";
- System.out.println(b);
- }
- public B() {
- System.out.println("Construct B");
- }
- }
- public class C {
- public static void main(String[] args) {
- new B();
- }
- }
以上輸出是:
Static A Static B A Construct A B Construct B |
一切都是java編譯器搞得鬼. JVM只是負責解析字節碼.字節碼雖然不是最原始的原子匯編碼,但字節碼已經可以完全解釋JVM的指令執行過程了.一般來說,字節碼和java源碼相差比較大,javac會做前期優化,修改增加刪除源碼產生jvm解釋器可以理解的字節碼. java語法帶來的安全,易用,易讀等功能讓我們忽略了字節碼會和java源碼有出路.
當遇到new的時候,比如new B(),將會嘗試去初始化B類.如果B已經初始化,則開始實例化B類.如果B類沒有初始化,則初始化B類,但B類繼承A,所以在初始化B類之前需要先初始化A類.所以類的初始化過程是:A->B. 類在初始化的時候會執行static域和塊. 類的實例化在類初始化之后,實例化的時候必須先實例化父類.實例化會先執行域和塊,然后再執行構造函數.
上面的理論如果靠這種死記硬背,總會忘記.哦,還有父類的構造函數必須放在子類構造函數的***行.為什么?
遇到這種語法問題的時候,看教科書不如自己找出答案.工具就在JDK中,一個名叫javap的命令. javap會打出一個class的字節碼偽碼. 我們只需要分析B的字節碼,就可以找到答案.
- joeytekiMacBook-Air:bin joey$ javap -verbose B
- Compiled from "B.java"
- public class B extends A
- SourceFile: "B.java"
- minor version: 0
- major version: 50
- Constant pool:
- const #1 = class #2; // B
- const #2 = Asciz B;
- const #3 = class #4; // A
- const #4 = Asciz A;
- const #5 = Asciz b;
- const #6 = Asciz Ljava/lang/String;;
- const #7 = Asciz j;
- const #8 = Asciz <clinit>;
- const #9 = Asciz ()V;
- const #10 = Asciz Code;
- const #11 = String #12; // NB
- const #12 = Asciz NB;
- const #13 = Field #1.#14; // B.b:Ljava/lang/String;
- const #14 = NameAndType #5:#6;// b:Ljava/lang/String;
- const #15 = String #16; // Static B
- const #16 = Asciz Static B;
- const #17 = Field #18.#20; // java/lang/System.out:Ljava/io/PrintStream;
- const #18 = class #19; // java/lang/System
- const #19 = Asciz java/lang/System;
- const #20 = NameAndType #21:#22;// out:Ljava/io/PrintStream;
- const #21 = Asciz out;
- const #22 = Asciz Ljava/io/PrintStream;;
- const #23 = Method #24.#26; // java/io/PrintStream.println:(Ljava/lang/String;)V
- const #24 = class #25; // java/io/PrintStream
- const #25 = Asciz java/io/PrintStream;
- const #26 = NameAndType #27:#28;// println:(Ljava/lang/String;)V
- const #27 = Asciz println;
- const #28 = Asciz (Ljava/lang/String;)V;
- const #29 = Asciz LineNumberTable;
- const #30 = Asciz LocalVariableTable;
- const #31 = Asciz <init>;
- const #32 = Method #3.#33; // A."<init>":()V
- const #33 = NameAndType #31:#9;// "<init>":()V
- const #34 = Field #1.#35; // B.j:Ljava/lang/String;
- const #35 = NameAndType #7:#6;// j:Ljava/lang/String;
- const #36 = String #2; // B
- const #37 = String #38; // Construct B
- const #38 = Asciz Construct B;
- const #39 = Asciz this;
- const #40 = Asciz LB;;
- const #41 = Asciz SourceFile;
- const #42 = Asciz B.java;
- {
- static {};
- Code:
- Stack=2, Locals=0, Args_size=0
- 0: ldc #11; //String NB
- 2: putstatic #13; //Field b:Ljava/lang/String;
- 5: ldc #15; //String Static B
- 7: putstatic #13; //Field b:Ljava/lang/String;
- 10: getstatic #17; //Field java/lang/System.out:Ljava/io/PrintStream;
- 13: getstatic #13; //Field b:Ljava/lang/String;
- 16: invokevirtual #23; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 19: return
- LineNumberTable:
- line 3: 0
- line 11: 5
- line 12: 10
- line 13: 19
- public B();
- Code:
- Stack=2, Locals=1, Args_size=1
- 0: aload_0
- 1: invokespecial #32; //Method A."<init>":()V
- 4: aload_0
- 5: ldc #11; //String NB
- 7: putfield #34; //Field j:Ljava/lang/String;
- 10: aload_0
- 11: ldc #36; //String B
- 13: putfield #34; //Field j:Ljava/lang/String;
- 16: getstatic #17; //Field java/lang/System.out:Ljava/io/PrintStream;
- 19: aload_0
- 20: getfield #34; //Field j:Ljava/lang/String;
- 23: invokevirtual #23; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 26: getstatic #17; //Field java/lang/System.out:Ljava/io/PrintStream;
- 29: ldc #37; //String Construct B
- 31: invokevirtual #23; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 34: return
- LineNumberTable:
- line 15: 0
- line 4: 4
- line 6: 10
- line 7: 16
- line 16: 26
- line 17: 34
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 35 0 this LB;
- }
類的生命周期,將經歷類的裝載,鏈接,初始化,使用,卸載. 裝載是將字節碼讀入到內存的方法區中, 而類的初始化則會在線程棧中執行static{}塊的code. 在之前,這個塊有另一個名字<cinit>即類初始化方法.現在改名為static{}了. 類的初始化只進行一次. 但是,每當一個類在裝載和鏈接完畢以后,通過字節碼的分析,JVM解析器已經知道B是繼承A的,于是在初始化B類前,A類會先初始化.這是一個遞歸過程. 所以,B類的初始化會導致A類static{}執行,然后是B的static{}執行.讓我們看看B的static{}塊中執行了什么.
- static {};
- Code:
- Stack=2, Locals=0, Args_size=0
- 棧深為2,本地變量0個,參數傳遞0個.
- 0: ldc #11; //String NB
- 將常量池中#11放到棧頂.#11="NB".
- 2: putstatic #13; //Field b:Ljava/lang/String;
- 將棧頂的值 "NB" 賦予常量池中的#13,也就是 static b="NB".
- 5: ldc #15; //String Static B
- 將#15放入棧頂. #15="static B".
- 7: putstatic #13; //Field b:Ljava/lang/String;
- 賦值static b = "static B".
- 10: getstatic #17; //Field java/lang/System.out:Ljava/io/PrintStream;
- 將PrintStream引用壓棧.
- 13: getstatic #13; //Field b:Ljava/lang/String;
- 將static b的值壓棧.
- 16: invokevirtual #23; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 調用虛函數PrintStream.println("static B")
- 19: return
- 退出函數,銷毀函數棧幀.
通過注釋,我們看到類B中的static域賦值和static塊均被放到了類的初始化函數中.
當我們進行類的實例化的時候,會調用類的構造函數.我們看看類B的構造函數做了什么.
- public B();
- Code:
- Stack=2, Locals=1, Args_size=1
- 棧深為2,本地變量1個(其實就是this),參數為1個(就是this).
- 0: aload_0
- 將***個參數壓棧.也就是this壓棧.
- 1: invokespecial #32; //Method A."<init>":()V
- 在this上調用父類的構造函數.在B的構造函數中并沒有聲明super(),但是java編譯器會自動生成此字節碼來調用父類的無參構造函數.如果在B類中聲明了super(int),編譯器會使用對應的A類構造函數來代替.JVM只是執行字節碼而已,它并不對super進行約束,約束它們的是java的編譯器.this出棧.
- 4: aload_0
- 將this壓棧.
- 5: ldc #11; //String NB
- 將"NB"壓棧.
- 7: putfield #34; //Field j:Ljava/lang/String;
- 給j賦值this.j="NB". this和"NB"出棧.
- 10: aload_0
- 將this壓棧.
- 11: ldc #36; //String B
- 把"B"壓棧
- 13: putfield #34; //Field j:Ljava/lang/String;
- 給j賦值this.j="B". this和"B"出棧.棧空
- 16: getstatic #17; //Field java/lang/System.out:Ljava/io/PrintStream;
- 壓棧PrintStream
- 19: aload_0
- 壓棧this
- 20: getfield #34; //Field j:Ljava/lang/String;
- this出棧,調用this.j,壓棧this.j.
- 23: invokevirtual #23; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 調用PrintStream.println(this.j).棧空.
- 26: getstatic #17; //Field java/lang/System.out:Ljava/io/PrintStream;
- 壓棧PrintStream
- 29: ldc #37; //String Construct B
- 壓棧"Construct B"
- 31: invokevirtual #23; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 調用PrintStream.println("Construct B")
- 34: return
從上面的字節碼可以看出,java編譯器在編譯產生字節碼的時候,將父類的構造函數,域的初始化,代碼塊的執行和B的真正的構造函數按照順序組合在了一起,形成了新的構造函數. 一個類的編譯后的構造函數字節碼一定會遵循這樣的順序包含以下內容:
父類的構造函數->
當前類的域初始化->(按照書寫順序)
代碼塊->(按照書寫順序)
當前類的構造函數.
到這里,應該徹底明白繼承類的初始化和實例化順序了.