1. 程式人生 > >物件的初始化過程及其深入理解

物件的初始化過程及其深入理解

一、什麼是一個類

        在Java語言裡面,類用class描述,擁有變數和函式。沒有提供get或set方法的變數稱之為欄位。有get或者set的任意方法或全部方法的欄位稱之為屬性。所有的類都繼承自Object類,並且繼承了Object類提供的class屬性。
        類和物件的關係,簡單說:
        類可以看成一類物件的模板,物件可以看成該類的一個具體例項。

二、子類和父類

        繼承是從父類中派生出新的類,這個類稱之為子類或者派生類。子類擁有父類“所有”的屬性和方法,並且子類能夠擴充套件其它特性。比如說人,人可以作為一個父類,它可以派生出子類,比如男人,男人具備了人的一切特性。並且男人還有其它特性,比如說長鬍子。

        如果父類的屬性或者方法是private修飾的,那麼父類的屬性和方法依然會被子類繼承到。但是,如果父類沒有提供公共的訪問方法的話,那麼子類將無法訪問到此屬性或者方法,即使子類擁有此屬性或者方法。舉個例子,我是潘多拉的繼承人,並且我繼承到了潘多拉留下的魔盒,但是潘多拉並沒有提供鑰匙給我,所以儘管我現在擁有了潘多拉魔盒,但是仍然不能開啟盒子。這就是擁有但是不能訪問的現象。

        在Java中,通常父類用private修飾的屬性會提供公共的訪問方法,通常是public修飾的get方法。(相當於例子中的鑰匙)

三、物件的初始化過程

        載入某個類到JVM中,由第一次呼叫這個類的靜態成員觸發。而建構函式又是一種特殊的靜態函式

,因此new一個物件的時候,JVM虛擬機器將會開始載入這個類。

        類是物件的模版,物件是類的具體例項。

        通常物件的初始化過程如下:

                步驟1:第一次呼叫類的建構函式建立物件,也就是說第一次呼叫一個類的靜態成員的時候,JVM將會動態的載入這個類到JVM中。用來描述這個類資訊的是一個class物件。JVM載入的就是這個class物件。如果這個類有靜態程式碼塊,那麼此時靜態程式碼塊將會被執行。

                步驟2:class物件載入完畢。接下來就是將此類中所有的屬性將會被設定為預設值。比如,int型別的屬性的預設是 0,  boolean型別的屬性預設值是false等。此步驟可以理解為“預設初始化”。

                 步驟3:接著,執行建構函式的第一行,通常都是super()。基本上每個建構函式第一行都會有一句隱式super(),它將會呼叫父類的建構函式。子類來自於父類,所以父類的class物件也已經被JVM載入了。根據第二步,父類中所有的屬性都被“預設初始化”為預設值。如果父類是Object的話,super()執行完畢都沒有什麼效果。現在,根據使用者的定義, 比如有int age = 22;  在記憶體中,經過步驟2,age預設值是“0”。步驟3的作用就是把age初始化為"22",這一步可以理解為“顯示初始化”。

                步驟4:顯示初始化完畢,將會呼叫"構造程式碼塊"。構造程式碼塊的作用是為這個類的每一個物件進行屬性的初始化。 如果我們沒有在步驟3裡面為欄位進行“顯示初始化”,那麼我們可以在“構造程式碼塊”中為欄位進行初始化。這步可以理解為“構造程式碼塊初始化”

                步驟5:構造程式碼塊執行完畢,就下來就是執行建構函式中剩下的語句了。

例如

/**
 * Created by jay.zhou on 2018/2/22.
 */
public class Fu {
    public int count = 1;
    static {
        System.out.println("父類的class檔案被JVM載入,類中所有欄位被設定為預設值,int型別的值的預設值是0");
    }

    {
        System.out.println(count);
        System.out.println("根據count的值知道,count的值被顯示初始化為1");
        System.out.println("父類的構造程式碼塊執行");
        count = 2;
        System.out.println("count的值已經被修改");
    }

    public Fu() {
        super();
        System.out.println("父類的建構函式執行");
        System.out.println(count);
    }
}

class Zi extends Fu {
    static {
        System.out.println("子類的class檔案被JVM載入,類中所有欄位被設定為預設值,int型別的值的預設值是0");
    }

    {
        System.out.println("子類的構造程式碼塊執行");
    }

    public Zi() {
        super();
        System.out.println("子類的建構函式執行");
    }

    public static void main(String[] args) {
        new Zi();
    }
}
父類的class檔案被JVM載入,類中所有欄位被設定為預設值,int型別的值的預設值是0
子類的class檔案被JVM載入,類中所有欄位被設定為預設值,int型別的值的預設值是0
1
根據count的值知道,count的值被顯示初始化為1
父類的構造程式碼塊執行
count的值已經被修改
父類的建構函式執行
2
子類的構造程式碼塊執行
子類的建構函式執行

解釋:

         在main()中,呼叫子類的建構函式,由於建構函式是一種特殊的靜態函式,將會觸發JVM載入子類。子類的存在必須依附於其父類,原因在下面會解釋。因此,父類與子類一樣,將會被載入到JVM虛擬機器中,並根據步驟1,呼叫它們的靜態程式碼塊。父類靜態程式碼塊優先於子類靜態程式碼塊執行,可以簡單的理解,先有了爸爸才會有兒子。

         根據步驟2,此時,類Fu和類Zi,它們的屬性的值都是預設值,比如Fu類中的count被設定為0。

         接著,呼叫 Zi類的建構函式,它的第一行是super()。那麼將會執行Fu類的建構函式。Fu類繼承於Object類,在Fu類的建構函式中,呼叫完super() 後,將不會產生任何效果。接著,根據步驟3,Fu類將會進行“顯示初始化”,根據private int count = 1; Fu類屬性count的值從“0”被賦值為了“1”。這一點在"構造程式碼塊"中的列印可以證明。

         根據步驟4,此時進行“構造程式碼塊”初始化。構造程式碼塊能為這個類的每個物件進行初始化,然而事實上很少有人使用構造程式碼塊初始化,基本上用的是在“建構函式中”進行初始化。

      “構造程式碼塊”執行完畢,將會執行“建構函式初始化”,此時將會執行建構函式中進行的定義的語句。

        此時,在Zi類中的建構函式的super()語句就全部執行完畢了。Zi類將會像Fu類執行完super()語句一樣,進行“顯示初始化”,“構造程式碼塊初始化”,最後執行Zi類建構函式中剩下的語句。

       綜上:物件初始化過程的步驟是:

                 1.被JVM載入,執行靜態程式碼塊

                 2.預設初始化 ,  屬性被賦予預設值

                 3.顯示初始化,定義的時候被賦予什麼值,現在就是什麼值

                 4.構造程式碼塊初始化,構造程式碼塊中重新為屬性賦予新值

                 5.最後是建構函式初始化,一錘定音的還是建構函式中賦予的值

物件的初始化過程較為複雜,如果有沒有看懂的歡迎留言。

四、子類物件與父類物件的聯絡

        類好比一張圖紙。在建立物件的過程中,子類物件根據子類的圖紙,創建出來的物件,為什麼它能夠訪問到從父類繼承到的屬性呢?問題是父類也是一張圖紙,我們在建立子類物件的時候,並沒有呼叫父類的建構函式,去建立一個父類物件。
比如  new Zi(); 為什麼這個物件它擁有父類的count這個欄位
        子類物件與父類物件的關係,如圖所示。

           

           此圖說明,在初始化子類物件之前,父類物件就已經被初始化完畢。

           子類物件能夠訪問到父類物件的屬性,這說明每一個子類物件都獨立的擁有一個屬於它自己的父類物件

因此,可以說,子類物件的建立必須依附於父類物件。在建立子類物件的過程中,先建立一個父類物件A,然後建立一個子類物件B,最後把A物件安裝到B物件中去。

五、要點

          載入一個類到JVM中,由第一次呼叫這個類的靜態成員觸發。建構函式其實是一種特殊的靜態函式。

          每個子類物件都獨立的擁有一個屬於它自己的父類物件。