一,疑問
從學習java至今,我一直對構造方法存在很多疑問,在此將我的疑問throw,你們可以catch到嗎?
面試官 :你說下構造方法吧!
我 :((⊙o⊙)… ,構造方法有什麼聊的,對象的new不是一直在用嗎?) 構造方法是一種特殊的方法,它是一個與類同名且沒有返回值類型的方法。對象的創建就是通過構造方法來完成,其功能主要是完成對象的初始化。當類實例化一個對象時會自動調用構造方法。構造方法和其他方法一樣也可以重載。
面試官 :你說構造方法功能是完成對象的初始化,類默認的構造方法怎麼完成對象的初始化,它裏面有沒有代碼哦!!!
我:(是啊,默認的構造方法沒有代碼,有也是super,怎麼初始化的)這個... 屬性不是都有默認值得嗎? int類型是0,引用類型是null....
面試官 :解釋下 private int i = 100; 這個i的值也是 0嗎? 如果不是 0,是100,什麼時候給賦的值? 或者是構造方法賦的值嗎? 如果不是構造方法賦的值,代碼裏能看到那條代碼給賦的值嗎?
我 :代碼裏的int i = 100;不是給賦了100嗎,就是這條代碼給賦的值。
面試官:額,方法的調用,執行估計你也應該清楚,沒有特殊邏輯基本都是順序執行,Student stu =new Student(); 調用了Student的無參構造方法,這個構造方法基本沒什麼代碼,你也清楚,請問 private int i = 100; 這條代碼什麼時候執行,構造方法顯然沒有調用這個代碼!對吧!
我 :(撓頭,代碼沒有執行這條複製語句啊?)可能是Java的設計者在構造方法中隱式執行了屬性的賦值語句....(我tm機智了,哈哈哈)
面試官 :正確與否先不說,你的說法可以解釋通,還有個問題,我要在構造方法裏面打印這個 i ,會不會存在賦值與打印併發的現象,或者我打印在前,賦值在後引起了錯誤?
我 : 我想規則應該是賦值永遠在代碼最前面,不會出現這個問題...(鬆了口氣,應該可以了吧!)
面試官:根據你的邏輯,能否解釋下 public static String stuClass = "碼農大學-頸椎病康復班";
我 : 解釋什麼???(一頭霧水)
面試官: 我的意思是,這stuClass是個靜態屬性,我可以直接通過類名調用,我並沒有new Studnet(),沒有調用構造方法,這個stuClass是怎麼賦值的?你的構造方法隱式賦值理論貌似解釋不通啊?
我: 這個...... (我的心在滴血.....)
我的部分疑問,通過面試中的博弈呈現出來(面試過程爲自己思考中的博弈,並非真實發生的),您能否解釋這些問題....
- new Student() 的過程發生了什麼?
- 構造方法幹了什麼?
- 屬性在什麼時候初始化(賦值)?
- 靜態屬性在什麼時候初始化(賦值)?
- .............
二,分析
接下來我們通過代碼分析。
2.1 靜態變量的賦值與靜態代碼塊的執行
父類相關代碼
//父類
public class Parent {
/**父類的靜態屬性*/
public static String preson;
public static String presonn = "人類";
/**父類的屬性*/
protected String name;
protected String namee = "碼農中的吳秀波";
protected int age;
protected int agee = 18;
/**
* 父類的初始化代碼塊
*/
{
System.out.println("Parent init ...");
System.out.println("Parent init age:" + age);
System.out.println("Parent init name:"+ name);
System.out.println("Parent init namee:"+ namee);
namee = "碼農中的吳彥祖";
}
/**
* 父類中的靜態初始化代碼塊
*/
static{
System.out.println("Parent cinit ...");
System.out.println("Parent cinit preson:"+preson);
System.out.println("Parent cinit presonn:" + presonn );
presonn = "人";
}
/**
* 父類的無參構造方法
*/
public Parent() {
// TODO Auto-generated constructor stub
System.out.println("Parent constructor ...");
System.out.println("Parent constructor namee:"+ namee);
}
}
子類的相關代碼
//子類
public class Student extends Parent{
/**子類的靜態屬性*/
public static String stuclass = "碼農大學-頸椎病康復班";
/**子類的屬性*/
private int stuNum = 10001;
/**
* 子類的初始化代碼塊
*/
{
System.out.println("Student init ...");
System.out.println("Student init stuNum :"+ stuNum);
}
/**
* 子類的靜態代碼塊
*/
static{
System.out.println("Student cinit ...");
System.out.println("Student cinit stuclass:"+ stuclass);
}
/**
* 子類無參構造方法
*/
public Student() {
// TODO Auto-generated constructor stub
System.out.println("Student constructor ...");
}
public static void main(String[] args) {
}
}
代碼相對比較簡單,如果只關心程序入口,即子類Student的main方法,運行這個main方法後,您能預計結果是什麼嗎?
//console 輸出
Parent cinit ...
Parent cinit preson:null
Parent cinit presonn:人類
Student cinit ...
Student cinit stuclass:碼農大學-頸椎病康復班
輸出的結果和您預期的一樣嗎?
通過輸出結果我們可知,一個空的main方法的運行,執行順序爲:
- 父類靜態變量的初始化(靜態代碼塊可以輸出靜態變量的值);
- 父類靜態代碼塊的執行(靜態代碼塊可以重新給靜態變量賦值,暫不演示了);
- 子類靜態變量的初始化;
- 子類靜態代碼塊的執行;
疑問一 :main 方法中並未出現代碼,main方法的執行爲什麼會有如上的輸出???
2.2 變量的賦值、初始化代碼的運行以及構造方法的運行
public static void main(String[] args) {
Student stu = new Student();
}
執行如上代碼,您能預測出輸出結果嗎?
//console 輸出
Parent cinit ...
Parent cinit preson:null
Parent cinit presonn:人類
Student cinit ...
Student cinit stuclass:碼農大學-頸椎病康復班
--------------------瘋哥線---------------------------
Parent init ...
Parent init age:0
Parent init name:null
Parent init namee:碼農中的吳秀波
Parent constructor ...
Parent constructor namee:碼農中的吳彥祖
Student init ...
Student init stuNum :10001
Student constructor ...
和您預期的一樣嗎?
通過輸出結果可知,mian 方法以及 Student stu = new Student();代碼,執行了
- 靜態相關輸出,同空的mian 方法
- 父類變量的初始化
- 父類的初始化代碼塊
- 父類的構造方法
- 子類變量的初始化
- 子類的初始化代碼塊
- 子類的構造方法
疑問二: Student stu = new Student(); 此語句按常理應該只執行父類和子類的構造方法,爲什麼會有如此的輸出(執行流程)?
三,解釋-結論-注意
3.1 疑問一的解釋
Java程序語言 -> 編譯器 -> .class -> JVM運行.class ;
Java編譯器會把靜態變量的初始化和靜態代碼塊順序在字節碼中生成爲<clinit>方法,此方法稱之爲類的構造器;
虛擬機運行字節碼有如下步驟(想要深入瞭解可以學習JVM、字節碼相關知識):加載->連接(驗證、準備、解析)->初始化->使用->卸載
初始化(執行<clinit>方法):
- 遇到new、getstatic、putstatic、invokestatic執行初始化操作
- 使用java.lang.reflect包的方法對類進行調用時,如果類沒有進行過初始化,則需要先觸發其初始化
- 當初始化一個類時,如果發現父類還沒有進行過初始化過,則需要先觸發其父類的初始化
- 當JVM啓動時,用戶需要指定一個要執行的類(包含main()方法的那個類),虛擬機會先初始化這個類
如上的初始化(加粗)步驟,完美解釋了 疑問一:main 方法中並未出現代碼,main執行爲什麼如上的輸出???
虛擬機啓動,我們指定了student類作爲啓動類(main方法),虛擬機加載...初始化這個類,即運行字節碼中的<clinit>方法,發現父類也沒有初始化,觸發其父類的初始化;
問題:您能舉出不被初始化的例子嗎???
通過 javap -verbose Student.class 命令查看類的字節碼
字節碼文件-類的構造方法3.2 疑問二的解釋
字節碼文件-實例的初始化方法通過main()的字節碼可以看出,Student stu = new Student();轉換成字節碼指令顯然不是一條(JVM是以一條條指令爲單位運行的),指令invokespecial:調用實例初始化,父類初始化和私有方法(調用了<init>方法)。
<init>:會將變量初始化,初始化語句塊,構造器方法等操作順序封裝到該方法中(父類先於子類運行);
這樣就可以解釋疑問二了,Student stu = new Student();雖然只有一條代碼,但虛擬機運行的字節碼指令可不單單一條指令;
3.3 總結
<clinit>方法是在類加載過程中執行的,而<init>是在對象實例化執行的,所以<clinit>一定比<init>先執行。即執行順序爲:
- 父類的靜態變量初始化
- 父類的靜態代碼塊
- 子類的靜態變量初始化
- 子類的靜態代碼塊
- 父類的變量初始化
- 父類的初始化代碼塊
- 父類的構造方法
- 子類的變量初始化
- 子類的初始化代碼塊
- 子類的構造方法