1. 程式人生 > 實用技巧 >java基礎(五)-----new一個物件的具體過程

java基礎(五)-----new一個物件的具體過程

在建立物件之前,首先要判斷類有沒有被載入,例如建立物件或呼叫類的static方法變數時,會觸發類載入,如下:

Dog dog = new Dog(); 

首次訪問某個類的靜態方法或者靜態欄位時:

Dog.staticFields;

類載入機制

java是使用雙親委派模型來進行類的載入的,所以在描述類載入過程前,我們先看一下它的工作過程:

雙親委託模型的工作過程是:如果一個類載入器(ClassLoader)收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委託給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父類載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需要載入的類)時,子載入器才會嘗試自己去載入。

使用雙親委託機制的好處是:能夠有效確保一個類的全域性唯一性,當程式中出現多個限定名相同的類時,類載入器在執行載入時,始終只會載入其中的某一個類。

1、載入

由類載入器負責根據一個類的全限定名來讀取此類的二進位制位元組流到JVM內部,並存儲在執行時記憶體區的方法區,然後將其轉換為一個與目標型別對應的java.lang.Class物件例項

2、驗證

格式驗證:驗證是否符合class檔案規範

語義驗證:檢查一個被標記為final的型別是否包含子類;檢查一個類中的final方法是否被子類進行重寫;確保父類和子類之間沒有不相容的一些方法宣告(比如方法簽名相同,但方法的返回值不同)

操作驗證:在運算元棧中的資料必須進行正確的操作,對常量池中的各種符號引用執行驗證(通常在解析階段執行,檢查是否可以通過符號引用中描述的全限定名定位到指定型別上,以及類成員資訊的訪問修飾符是否允許訪問等)

3、準備

為類中的所有靜態變數分配記憶體空間,併為其設定一個初始值(由於還沒有產生物件,例項變數不在此操作範圍內)

被final修飾的static變數(常量),會直接賦值;

4、解析

將常量池中的符號引用轉為直接引用(得到類或者欄位、方法在記憶體中的指標或者偏移量,以便直接呼叫該方法),這個可以在初始化之後再執行。

解析需要靜態繫結的內容。 // 所有不會被重寫的方法和域都會被靜態繫結

以上2、3、4三個階段又合稱為連結階段,連結階段要做的是將載入到JVM中的二進位制位元組流的類資料資訊合併到JVM的執行時狀態中。

5、初始化(先父後子)

5.1 為靜態變數賦值

5.2 執行static程式碼塊

注意:static程式碼塊只有jvm能夠呼叫

如果是多執行緒需要同時初始化一個類,僅僅只能允許其中一個執行緒對其執行初始化操作,其餘執行緒必須等待,只有在活動執行緒執行完對類的初始化操作之後,才會通知正在等待的其他執行緒。

因為子類存在對父類的依賴,所以類的載入順序是先載入父類後加載子類,初始化也一樣。不過,父類初始化時,子類靜態變數的值也有有的,是預設值。

最終,方法區會儲存當前類類資訊,包括類的靜態變數、類初始化程式碼(定義靜態變數時的賦值語句 和 靜態初始化程式碼塊)、例項變數定義、例項初始化程式碼(定義例項變數時的賦值語句例項程式碼塊和構造方法)和例項方法,還有父類的類資訊引用。

建立物件

類沒有父類的情況(父類為Object不算,因為Object是所有類的父類)

1.第一次建立 Dog 物件先執行上面的類載入階段

2.在堆上為 Dog 物件分配足夠的儲存空間,所有屬性和方法都被設定成預設值(數字為 0,字元為 null,布林為 false,而所有引用被設定成 null)

3.執行建構函式檢查是否有父類,如果有父類會先呼叫父類的建構函式,這裡假設 Dog 沒有父類,執行預設值欄位的賦值即方法的初始化動作。

4.執行建構函式。

有父類的情況

假設:

Dog extends Animal

1、執行第一步,找出 Dog.class 檔案,接著在載入過程中發現他有一個基類(通過 extends 關鍵字),於是先執行 Animal 類的第一二步,載入其靜態變數和方法,載入結束之後再載入子類 Dog 的靜態變數和方法。

如果 Animal 類還有父類就以此類推,最終的基類叫做根基類。

注意:因為子類的 static 初始化可能會依賴於父類的靜態資源,所以要先載入父類的靜態資源。

2、接著要 new Dog 物件,先為 Dog 物件分配儲存空間 -> 到 Dog 的建構函式 -> 建立預設的屬性。這裡其建構函式裡面的第一行有個隱含的 super(),即父類建構函式,所以這時會跳轉到父類 Animal 的建構函式。

Java 會幫我們完成建構函式的補充,Dog 實際隱式的建構函式如下:

Dog() { 
   //建立預設屬性和方法(成員變數等)
//呼叫父類的建構函式super()(可顯式寫出) //對預設屬性和方法分別進行賦值和初始化 }

3、父類 Animal 執行建構函式前也是分配儲存空間 -> 到其建構函式 -> 建立預設的屬性 -> 發現挖槽我已經沒有父類了,這個時候就給它的預設的屬性賦值和方法的初始化。

4、接著執行建構函式餘下的部分,結束後跳轉到子類 Dog 的建構函式。

5、子類 Dog 對預設屬性和方法分別進行賦值和初始化,接著完成建構函式接下來的部分。

因此,對於構造器有這樣的規定

  • 子類中所有的構造器預設都會訪問父類中空引數的構造器
  • 當父類中沒有空引數的構造器時,子類的構造器必須通過this(引數列表)或者super(引數列表)語句指定呼叫本類或者父類中相應的構造器。同時,只能”二選一”,且必須放在構造器的首行
  • 如果子類構造器中既未顯式呼叫父類或本類的構造器,且父類中又沒有無參的構造器,則編譯出錯

問題

1、為什麼要執行父類 Animal 的構造方法才繼續子類 Dog 的屬性及方法賦值?

因為子類 Dog 的非靜態變數和方法的初始化有可能使用到其父類 Animal 的屬性或方法,所以子類構造預設的屬性和方法之後不應該進行賦值,而要跳轉到父類的構造方法完成父類物件的構造之後,才來對自己的屬性和方法進行初始化。

這也是為什麼子類的建構函式顯示呼叫父類建構函式 super() 時要強制寫在第一行的原因,程式需要跳轉到父類建構函式完成父類物件的構造後才能執行子類建構函式的餘下部分。

2、為什麼對屬性和方法初始化之後再執行建構函式其他的部分?

因為建構函式中的顯式部分有可能使用到物件的屬性和方法。

Tips:其實這種初始化過程都是為了保證後面資源初始化用到的東西前面的已經初始化完畢了。

示例

//父類Animal
class Animal {
    /* 8、執行初始化 */
    private int i = 9;
    protected int j;

    /* 7、呼叫構造方法,建立預設屬性和方法,完成後發現自己沒有父類 */
    public Animal() {
        /* 9、執行構造方法剩下的內容,結束後回到子類建構函式中 */
        System.out.println("i = " + i + ", j = " + j);
        j = 39;
    }

    /* 2、初始化根基類的靜態物件和靜態方法 */
    private static int x1 = print("static Animal.x1 initialized");

    static int print(String s) {
        System.out.println(s);
        return 47;
    }
}

//子類 Dog
public class Dog extends Animal {
    /* 10、初始化預設的屬性和方法 */
    private int k = print("Dog.k initialized");

    /*
     * 6、開始建立物件,即分配儲存空間->建立預設的屬性和方法。 遇到隱式或者顯式寫出的super()跳轉到父類Animal的建構函式。
     * super()要寫在建構函式第一行
     */
    public Dog() {
        /* 11、初始化結束執行剩下的語句 */
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }

    /* 3、初始化子類的靜態物件靜態方法,當然mian函式也是靜態方法 */
    private static int x2 = print("static Dog.x2 initialized");

    /*
     * 1、要執行靜態main,首先要載入Dog.class檔案,載入過程中發現有父類Animal,
     * 所以也要載入Animal.class檔案,直至找到根基類,這裡就是Animal
     */
    public static void main(String[] args) {

        /* 4、前面步驟完成後執行main方法,輸出語句 */
        System.out.println("Dog constructor");
        /* 5、遇到new Dog(),呼叫Dog物件的建構函式 */
        Dog dog = new Dog();
        /* 12、執行main函式餘下的部分程式 */
        System.out.println("Main Left");
    }
}

列印結果為:

static Animal.x1 initialized
static Dog.x2 initialized
Dog constructor
i = 9, j = 0
Dog.k initialized
k = 47
j = 39
Main Left