【Java面試題】對於構造方法的疑問?——與類的初始化

一,疑問

從學習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>先執行。即執行順序爲:

  • 父類的靜態變量初始化
  • 父類的靜態代碼塊
  • 子類的靜態變量初始化
  • 子類的靜態代碼塊
  • 父類的變量初始化
  • 父類的初始化代碼塊
  • 父類的構造方法
  • 子類的變量初始化
  • 子類的初始化代碼塊
  • 子類的構造方法
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章