關于類的對象創建與初始化

今天,我們就來解決一個問題,一個類實例究竟要經過多少個步驟才能被創建出來,也就是下面這行代碼的背后,JVM 做了哪些事情?

Object obj = new Object();

當虛擬機接受到一條 new 指令時,首先會拿指令后的參數,也就是我們類的符號引用,于方法區中進行檢查,看是否該類已經被加載,如果沒有則需要先進行該類的加載操作。

一旦該類已經被加載,那么虛擬機會根據類型信息在堆中分配該類對象所需要的內存空間,然后返回該對象在堆中的引用地址。

一般而言,虛擬機會在 new 指令執行結束后,顯式調用該類的對象的 <init> 方法,這個方法需要程序員在定義類的時候給出,否則編譯器將在編譯期間添加一個空方法體的 <init> 方法。

以上步驟完成后,基本上一個類的實例對象就算是被創建完成了,才能夠為我們程序中使用,下面我們詳細的了解每個步驟的細節之處。

初始化父類

知乎上看到一個問題:

Java中,創建子類對象時,父類對象會也被一起創建么?

有關這個問題,我還特意去搜了一下,很多人都說,一個子類對象的創建,會對應一個父類對象的創建,并且這個子類對象會保存這個父類對象的引用以便訪問父類對象中各項信息

這個答案肯定是不對的,如果每一個子類對象的創建都要創建其所有直接或間接的父類對象,那么整個堆空間豈不是充斥著大量重復的對象?這種內存空間的使用效率也會很低。

我猜這樣的誤解來源于 《Thinking In Java》 中的一句話,可能大家誤解了這段話,原話很多很抽象,我簡單總結了下:

虛擬機保證一個類實例初始化之前,其直接父類或間接父類的初始化過程執行結束

看一段代碼:

public class Father {
    public Father(){
        System.out.println("father's constructor has been called....");
    }
}
public class Son extends Father {
    public Son(){
        System.out.println("son's constructor has been called ...");
    }
}
public static void main(String[] args){
    Son son = new Son();
}

輸出結果:

father's constructor has been called....
son's constructor has been called ...

這里說的很明白,只是保證父類的初始化動作先執行,并沒有說一定會創建一個父類對象引用。

這里很多人會有疑惑,虛擬機保證子類對象的初始化操作之前,先完成父類的初始化動作,那么如果沒有創建父類對象,父類的初始化動作操作的對象是誰?

這就涉及到對象的內存布局,一個對象在堆中究竟由哪些部分組成?

HotSpot 虛擬機中,一個對象在內存中的布局由三個區域組成:對象頭,實例數據,對齊填充。

對象頭中保存了兩部分內容,其一是自身運行的相關信息,例如:對象哈希碼,分代年齡,鎖信息等。其二是一個指向方法區類型信息的引用。

對象實例數據中存儲的才是一個對象內部數據,程序中定義的所有字段,以及從父類繼承而來的字段都會被記錄保存。

像這樣:

image

當然,這里父類的成員方法和屬性必須是可以被子類繼承的,無法繼承的屬性和方法自然是不會出現在子類實例對象中了。

粗糙點來說,我們父類的初始化動作指的就是,調用父類的 <init> 方法,以及實例代碼塊,完成對繼承而來的父類成員屬性的初始化過程。

對齊填充其實也沒什么實際的含義,只是起到一個占位符的作用,因為 HotSpot 虛擬機要求對象的大小是 8 的整數倍,如果對象的大小不足 8 的整數倍時,會使用對齊填充進行補全。

所以不存在說,一個子類對象中會包含其所有父類的實例引用,只不過繼承了可繼承的所有屬性及方法,而所謂的「父類初始化」動作,其實就是對父類 <init> 方法的調用而已。

this 與 super 關鍵字

this 關鍵字代表著當前對象,它只能使用在類的內部,通過它可以顯式的調用同一個類下的其他方法,例如:

public class Son {

    public void sayHello(){
        System.out.println("hello");
    }
    public void introduce(String name){
        System.out.println("my name is:" + name);

        this.sayHello();
    }
}

因為每一個方法的調用都必須有一個調用者,無論你是類方法,或是一個實例方法,所以理論上,即便在同一個類下,調用另一個方法也是需要指定調用者的,就像這里使用 this 來調用 sayHello 方法一樣。

并且編譯器允許我們在調用同類的其他實例方法時,省略 this。

其實每個實例方法在調用的時候都默認會傳入一個當前實例的引用,這個值最終被傳遞賦值給變量 this。例如我們在主函數中調用一個 sayHello 方法:

public static void main(String[] args){
    Son son = new Son();
    son.sayHello();
}

我們反編譯主函數所在的類:

image

字節碼指令第七行,astore_1 將第四行返回的 Son 實例引用存入局部變量表,aload_1 加載該實例引用到操作數棧。

接著,invokevirtual #4 會調用一個虛方法(也就是一個實例方法),該方法的符號引用為常量池第四項,除此之外,編譯器還會將操作數棧頂的當前實例引用作為方法的一個參數傳入。

image

可以看到,sayHello 方法的局部變量表中的 this 的值 就是方法調用時隱式傳入的。這樣你在一個實例方法中不加 this 的調用其他任意實例方法,其實調用的都是同一個實例的其他方法。

總的來說,對于關鍵字 this 的理解,只需要抓住一個關鍵點就好:它代表的是當前類實例,并且每個非靜態方法的調用都必定會傳入當前的實例對象,而被調用的方法默認會用一個名為 this 的變量進行接收。

這樣做的唯一目的是,實例方法是可以訪問實例屬性的,也就是說實例方法是可以修改實例屬性數據值的,所以任何的實例方法調用都需要給定一個實例對象,否則這些方法將不知道讀寫哪個對象的屬性值。

那么 super 關鍵字又代表著誰,能夠用來做什么呢?

我們說了,一個實例對象的創建是不會創建其父類對象的,而是直接繼承的父類可繼承的字段,大致的對象內存布局如下:

image

this 關鍵字可以引用到當前實例對象的所有信息,而 super 則只能引用從直接父類那繼承來的成員信息。

看一段代碼:

public class Father {
    public String name = "father";
}
public class Son extends Father{
    public String name = "son";
    public void showName(){
        System.out.println(super.name);
        System.out.println(this.name);
    }
}

主函數中調用這個 showName 方法,輸出結果如下:

father
son

應該不難理解,無論是 this.name 或是 super.name 它們對應的字節碼指令是一樣的,只是參數不同而已。而這個參數,編譯器又是如何確定的呢?

如果是 this,編譯器優先從當前類實例中查找匹配的屬性字段,沒有找到的話將遞歸向父類中繼續查詢。而如果是 super 的話,將直接從父類開始查找匹配的字段屬性,沒有找到的話一樣會遞歸向上繼續查詢。

完整的初始化過程

下面我們以兩道面試題,加深一下對于對象的創建與初始化的相關細節理解。

面試題一:

public class A {
    static {
        System.out.println("1");
    }
    public A(){
        System.out.println("2");
    }
}
public class B extends A {
    static{
        System.out.println("a");
    }
    public B(){
        System.out.println("b");
    }
}

Main 函數調用:

public static void main(String[] args){
    A ab = new B();
    ab = new B();
}

大家不妨可以思考一下,最終的輸出結果是什么。

輸出結果如下:

1
a
2
b
2
b

我們來解釋一下,第一條語句:

A ab = new B();

首先發現類 A 并沒有被加載,于是進行 A 的類加載過程,類加載的最后階段,初始化階段會調用編譯器生成的 <clinit> 方法,完成類中所有靜態屬性的賦值操作,包括靜態塊的代碼執行。于是打印字符「1」。

緊接著會去加載類 B,同樣的過程,打印了字符「a」。

最后調用 new 指令,于堆上分配內存,并開始實例初始化操作,調用自身構造器之前會首先調用一下父類 A 的構造器保證對 A 的初始化,于是打印了字符「2」,接著調用字節的構造器,打印字符「b」。

至此,第一條語句算是執行結束了。

第二條語句:

ab = new B();

由于類型 B 已經被加載進方法區了,虛擬機不會重復加載,直接進入實例化的過程,同樣的過程,分別打印字符「2」和「b」。

這一道題目應該算簡單的,只要理解了類加載過程中的初始化過程和實例對象的初始化過程,應該是手到擒來。

面試題二:

public class X {
    Y y = new Y();
    public X(){
        System.out.println("X");
    }
}
public class Y {
    public Y(){
        System.out.println("Y");
    }
}
public class Z extends X {
    Y y  = new Y();
    public Z(){
        System.out.println("Z");
    }
}

Main 函數調用:

public static void main(String[] args){
    new Z();
}

同樣的,大家可以先自行分析分析運行的結果是什么。

輸出結果如下:

Y
X
Y
Z

我們一起來分析一下,首先這個主函數中的代碼很簡單,就是實例化一個 Z 類型的對象,虛擬機一樣的會先進行 Z 的類加載過程。

發現并沒有靜態語句需要執行,于是直接進入實例化階段。實例化階段主要分為三個部分,實例屬性字段的初始化,實例代碼塊的執行,構造函數的執行。 而實際上,對于實例屬性字段的賦值與實例代碼塊中代碼都會被編譯器放入構造函數中一起運行。

所以,在執行 Z 的構造器之前會先進入 X 的構造器,而 X 中的實例屬性會按序被編譯器放入構造器。也就是說,X 構造器的第一步其實是這條語句的執行:

Y y = new Y();

所以,進行類型 Y 的類加載與實例化過程,結束后會打印字符「Y」。

然后,進入 X 的構造器繼續執行,打印字符「X」。

至此,父類的所有初始化動作完成。

最后,進行 Z 本身的構造器的初始化過程,一樣會先初始化實例屬性,再執行構造函數方法體,輸出字符「Y」和「Z」。

有關類對象的創建與初始化過程,這兩道題目算是很好的檢驗了,其實這些初始化過程并不復雜,只需要你理解清楚各個步驟的初始化順序即可。


文章中的所有代碼、圖片、文件都云存儲在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:撲在代碼上的高爾基,所有文章都將同步在公眾號上。

image
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容

  • 官方文檔 初始化 Initialization是為準備使用類,結構體或者枚舉實例的一個過程。這個過程涉及了在實例里...
    hrscy閱讀 1,144評論 0 1
  • 20- 枚舉,枚舉原始值,枚舉相關值,switch提取枚舉關聯值 Swift枚舉: Swift中的枚舉比OC中的枚...
    iOS_恒仔閱讀 2,304評論 1 6
  • 面向對象程序設計概述 面向對象程序設計(簡稱OOP)是當今主流的程序設計范型,它已經取代了傳統的“結構化”過程化程...
    Steven1997閱讀 912評論 0 0
  • 學會講話是為人處世最基本的素養,學會講話是當今社會最需要的技能,或許頭腦簡單可以說是可愛,單純,但也可以...
    雨滴yu閱讀 332評論 0 0
  • 還記得曾經的我們嗎?那個單純得像張白紙的我們,無話不說的我們,牽手在校園的我們,陳舊的往昔...
    程雨鰈閱讀 356評論 1 2