從原理來理解繼承關係的類初始化和實例化的順序

就像以前的一個評論.咱們學習的是思路. 不少人都知道繼承關係的類的初始化和實例化的順序,但若是忘記了怎麼辦? 如何找到本身的答案? 又若是遇到的問題是關於泛型的擦除問題,又該如何去分析? java

思路,重點是思路.泛型擦除先不談.看繼承. 首先給出一個例子,看看它的輸出是什麼. 安全

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 jvm

一切都是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的構造函數作了什麼. this

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的真正的構造函數按照順序組合在了一塊兒,造成了新的構造函數. 一個類的編譯後的構造函數字節碼必定會遵循這樣的順序包含如下內容:
父類的構造函數->
當前類的域初始化->(按照書寫順序)
代碼塊->(按照書寫順序)
當前類的構造函數.

到這裏,應該完全明白繼承類的初始化和實例化順序了. 編碼

相關文章
相關標籤/搜索