摘要:很多時候提到類載入,大家總是沒法馬上回憶起順序,這篇文章會用一個例子為你把類載入的諸多問題一次性澄清。
本文分享自華為雲社群《用1個例子加5個問題,一次性搞清java中的類載入問題【奔跑吧!JAVA】》,原文作者:breakDraw 。
很多時候提到類載入,大家總是沒法馬上回憶起順序,這篇文章會用一個例子為你把類載入的諸多問題一次性澄清。
Java類的載入順序
引用1個網上的經典例子,並做稍許改動,以便大家更好地理解。
原例子引用自:https://blog.csdn.net/zfx2013/article/details/89453482
public class Animal { private int i = test(); private static int j = method(); static { System.out.println("a"); } Animal(){ System.out.println("b"); } { System.out.println("c"); } public int test(){ System.out.println("d"); return 1; } public static int method(){ System.out.println("e"); return 1; } } public class Dog extends Animal{ { System.out.println("h"); } private int i = test(); static { System.out.println("f"); } private static int j = method(); Dog(){ System.out.println("g"); } public int test(){ System.out.println("i"); return 1; } public static int method(){ System.out.println("j"); return 1; } public static void main(String[] args) { Dog dog = new Dog(); System.out.println(); Dog dog1 = new Dog(); } }
執行這段main程式,會輸出什麼?
答案是
eafjicbhig
icbhig
為了方便大家一個個細節去理解, 我換一種方式去提問。
Q: 什麼時候會進行靜態變數的賦值和靜態程式碼塊的執行?
A:
- 第一次建立某個類或者某個類的子類的例項
- 訪問類的靜態變數、呼叫類的靜態方法
- 使用反射方法forName
- 呼叫主類的main方法(本例子的第一次靜態初始化其實屬於這個情況,呼叫了Dog的main方法)
注: 類初始化只會進行一次, 上面任何一種情況觸發後,之後都不會再引起類初始化操作。
Q:初始化某個子類時,也會對父類做靜態初始化嗎?順序呢?
A:如果父類之前沒有被靜態初始化過,那就會進行, 且順序是先父類再子類。 後面的非靜態成員初始化也是如此。
所以會先輸出eafj。
Q: 為什麼父類的method不會被子類的method重寫?
A: 靜態方法是類方法,不會被子類重寫。畢竟類方法呼叫時,是必定帶上類名的。
Q: 為什麼第一個輸出的是e而不是a?
A: 因為類變數的顯示賦值程式碼和靜態程式碼塊程式碼按照從上到下的順序執行。
Animal的靜態初始化過程中,method的呼叫在static程式碼塊之前,所以先輸出e再輸出a。
而Dog的靜態初始化過程中,method的呼叫在static程式碼塊之後,因此先輸出f,再輸出j
Q: 沒有在子類的構造器中呼叫super()時,也會進行父類物件的例項化嗎?
A: 會的。會自動呼叫父類的預設構造器。 super()主要是用於需要呼叫父類的特殊構造器的情況。
因此會先進行Animal的物件例項化,再進行Dog的物件例項化
Q: 構造方法、成員顯示賦值、非靜態程式碼塊(即輸出c和h的那2句)的順序是什麼?
A:
- 成員顯示賦值、非靜態程式碼塊(按定義順序)
- 構造方法
因此Animal的例項化過程輸出icb(如果對輸出i有疑問,見下面一題)
接著進行Dog的例項化,輸出hig
Q: 為什麼Animal例項化時, i=test()中輸出的是i而不是d?
A:因為你真正建立的是Dog子類,Dog子類中的test()方法由於簽名和父類test方法一致,因此test方法被重寫了。
此時即使在父類中呼叫,也還是用使用子類Dog的方法。除非你new的是Animal。
Q: 同上題, 如果test方法都是private或者final屬性, 那麼上題的情況會有變化嗎??
A:
因為private和final方法是不能被子類重寫的。
所以Animal例項化時,i=test輸出d。
總結一下順序:
- 父類靜態變數顯式賦值、父類靜態程式碼塊(按定義順序)
- 子類靜態變數顯式賦值、子類靜態程式碼塊(按定義順序)
- 父類非靜態變數顯式賦值(父類例項成員變數)、父類非靜態程式碼塊(按定義順序)
- 父類建構函式
- 子類非靜態變數(子類例項成員變數)、子類非靜態程式碼塊(按定義順序)
- 子類建構函式。
類載入過程
Q:類載入的3個必經階段是:
A:
- 載入(類載入器讀取二進位制位元組流,生成java類物件)
- 連結(驗證,分配靜態域初始零值)
- 初始化(前面的題目講的其實就是初始化時的順序)
更詳細的如下:
被動引用中和類靜態初始化的關係
Q:new某個類的陣列時,會引發類初始化嗎?
像下面輸出什麼
public class Test { static class A{ public static int a = 1; static{ System.out.println("initA"); } } public static void main(String[] args) { A[] as = new A[5]; } }
A:
new陣列時,不會引發類初始化。
什麼都不輸出。
Q:引用類的final靜態欄位,會引發類初始化嗎?
像下面輸出什麼?
public class Test { static class A{ public static final int a = 1; static{ System.out.println("initA"); } } public static void main(String[] args) { System.out.println("A.a=" + A.a); } }
A: 不會引發。
不會輸出initA。 去掉final就會引發了。
(注意這裡必須是基本型別常量, 如果是引用型別產量,則會引發類初始化)
Q:子類引用了父類的靜態成員,此時子類會做類初始化嘛?
如下會輸出什麼
public class Test { static class A{ public static int a = 1; static{ System.out.println("initA"); } } static class B extends A{ static { System.out.println("initB"); } } public static void main(String[] args) { System.out.println("B.a=" + B.a); } }
A:
子類不會初始化。
列印initA,卻不會列印initB。
類載入器
雙親委派
類載入時的雙親委派模型,不知道能怎麼出題。。。反正就記得優先去父類載入器中看類是否能載入。
就貼個圖吧:
注意,上面的圖有問題。
Bootsrap不是ClassLoader的子類,他是C++編寫的。
而ExtClassLoader和AppClassLoader都是繼承自ClassLoader的
Q:java中, 是否類和介面的包名和名字相同, 那麼就一定是同一個類或者介面?
A:錯誤。
1個jvm中, 類和介面的唯一性由 二進位制名稱以及它的定義類載入器 共同決定。
因此2個不同的載入器載入出來相同的類或介面時, 實際上是不同的。