JVM初探(五):類的例項化

Createsequence發表於2020-08-09

一、概述

我們知道,一個物件在可以被使用之前必須要被正確地例項化。而例項化實際指的就是以一個java類為模板建立物件/例項的過程。比如說常見的 Person = new Person()程式碼就是一個將Person類例項化並建立引用的過程。

對於類的例項化,我們關注兩個問題:

  • 如何例項化?(類的四種例項化方式)
  • 什麼時候例項化?(類的一個初始化過程和物件的三個初始化過程)

二、類的四種例項化方式

1.使用new關鍵字

這也是最常見最簡單的建立物件的方法。通過這種方法,我們可以藉助類的建構函式例項化物件。

Parent p = new Parent();

2.使用newInstance()方法

我們可以先通過類的全限定名獲取類,然後通過Class類的newInstance()方法去呼叫類的無參構造方法建立一個物件:

Class p = Class.forName("com.huang.Parent");
Parent parent = (Parent) p.newInstance();

或者通過java.lang.relect.Constructor類裡的newInstance()方法去構造物件,這個方法比起Class自帶的更強大:

它可以呼叫類中有參構造方法私有構造方法建立物件!

//Parent私有的含參構造方法
public Parent(int a) {
    System.out.println("Parent建立了!");
}

//通過Constructor呼叫
Class p = Class.forName("com.huang.Parent");
Constructor<Parent> parentConstructor = p.getConstructor(int.class);
Parent parent = (Parent) p.newInstance();

3.使用clone()方法

當我們呼叫clone方法,JVM會幫我們建立一個新的、一樣的物件,特別需要說明的是,用clone方法建立物件的過程中並不會呼叫任何建構函式。這裡涉及到一個深拷貝和淺拷貝的知識點,我會另起一篇隨筆介紹,這裡就多費筆墨了。

Parent parent = new Parent();
Parent p2 = (Parent) parent.clone();

4.使用反序列化機制

當我們反序列化一個物件時,JVM會給我們建立一個單獨的物件,在此過程中,JVM並不會呼叫任何建構函式。

Parent parent = new Parent();

// 寫物件
ObjectOutputStream outputStream = new ObjectOutputStream(
    new FileOutputStream("parent.bin"));
outputStream.writeObject(parent);
outputStream.close();

// 讀物件
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(
    "parent.bin"));
Parent parent2 = (Parent) inputStream.readObject();

三、類例項化的過程

我們以 Person p = new Person()這條語句為例,當這條語句執行的時候,jvm進行了四步操作:

  • 先執行new語句,以Person類為模板,在堆中建立Person物件
  • 為Person物件執行構造方法(如果有父類會先執行父類構造方法)
  • 建立Person類的引用變數p
  • 將引用變數p指向記憶體中Person物件

我們不難看出,其實例項化的過程其實就是第一和第二步,在這兩步裡,jvm其實也進行了四步操作:

  • Person的初始化
  • Person物件變數的初始化(如果有父類會先執行父類變數的初始化)
  • Person物件程式碼塊的初始化
  • Person物件建構函式的初始化(如果有父類會先執行父類初始化)

1.類的初始化

對於第一次被例項化的物件,第一步是必定是類的初始化,所以靜態變數和靜態程式碼塊中的程式碼必然被賦值和執行。

這點在我關於類載入機制的文章中已有解釋,這裡就不多費筆墨。

2.物件變數的初始化

我們在定義物件中的變數的同時,還可以直接對物件變數進行賦值。它們會在建構函式執行之前完成這些初始化操作

//父類
public class Parent{
    int i = 1;
    int j = i + 1;
    
    public Parent() {
        System.out.println("Parent的構造方法執行了!");
        j += 10;
    }
}

//子類
public class Child extends Parent {

    int k = 1;
    int l = k + 1;

    public Child() {
        System.out.println("i:"+i);
        System.out.println("j:"+j);
        System.out.println("k:"+k);
        System.out.println("l:"+l);
        System.out.println("Child的構造方法執行了!");
        k += 8;
        System.out.println("k:"+k);
        System.out.println("l:"+l);
    }
}

public static void main( String[] args ) {
    Child child = new Child();
}
//執行結果
Parent的構造方法執行了!
i:1
j:12
k:1
l:2
Child的構造方法執行了!
k:9
l:2

我們可以知道執行順序是這樣的:

  • 父類的變數初始化i = 1,j=2;
  • 執行父類的建構函式j = 2 + 10 = 12;
  • 子類的變數初始化k = 1,l = 2;
  • 執行子類建構函式k = 1 + 8 = 9

這裡有人認為父類的變數初始化了,而且父類的建構函式也執行了,那父類是不是也一起例項化了?

答案是沒有,我們可以認為例項化的時候子類從父類一起拷貝了一份變數,建構函式的執行也是為了能讓父類的變數初始化,最後例項化放到記憶體裡的其實是子類+父類的一個混合體!

3.程式碼塊的初始化

我們一般指的程式碼塊是構造程式碼塊和靜態程式碼塊,靜態程式碼塊在類初始化時就執行,而構造程式碼塊在類一建立就執行,也優先於構造方法

我們舉個例子:

//父類
public class Parent{
    {
        System.out.println("Child的程式碼塊被執行了!");
    }
    public Parent() {
        System.out.println("Parent建立了!");
    }
}

//子類
public class Child extends Parent {

    public Child() {
        System.out.println("Child建立了!");
    }

    static {
        System.out.println("Child的構造方法執行了!");
    }

    {
        System.out.println("Child的程式碼塊被執行了!");
    }
}

//執行程式碼
public static void main( String[] args ) {
    Child child = new Child();
}

//列印結果
Parent的程式碼塊被執行了!
Parent的構造方法執行了!
Child的程式碼塊被執行了!
Child的構造方法執行了!

我們可以知道執行順序是這樣的:

  • 父類程式碼塊
  • 父類的構造方法
  • 子類的程式碼塊
  • 子類的構造方法

4.建構函式的初始化

我們可以從上文知道,例項變數初始化與例項程式碼塊初始化總是發生在建構函式初始化之前,那麼我們下面著重看看建構函式初始化過程。眾所周知,每一個Java中的物件都至少會有一個建構函式,如果我們沒有顯式定義建構函式,那麼它將會有一個預設無參的建構函式。在編譯生成的位元組碼中,這些建構函式會被命名成<init>()方法。

事實上,Java強制要求Object物件之外的所有物件建構函式的第一條語句必須是父類建構函式的呼叫語句,如果沒有就會預設生成謳歌建構函式。這就保證了不管要例項化的類繼承了多少父類,我們最終都能讓例項繼承到所有從父類繼承到的屬性。

5.小結

結合以上文,我們可以看出類的例項化其實是一個遞迴的過程。

從子類不斷向上遞迴,然後一直遞迴到直到抵達基類Object,然後一層一層的返回,先完成類的初始化:

  • 如果有類未初始化就先初始化(初始化靜態塊)

再回到Object類,往下一層一層的返回,完成物件的三個初始化:

  • 初始化變數
  • 初始化程式碼塊
  • 初始化建構函式

類例項化的遞迴過程

所以最終我們可以總結出類初始化過程中類的各種程式碼塊的執行順序:

  • 父類靜態塊
  • 子類靜態塊
  • 父類程式碼塊
  • 父類建構函式
  • 子類程式碼塊
  • 子類建構函式

驗證一下:

//父類
public class Parent{
    static {
        System.out.println("Parent的靜態塊執行了!");
    }

    public Parent() {
        System.out.println("Parent的構造方法執行了!");
    }

    {
        System.out.println("Parent的程式碼塊被執行了!");
    }
}

//子類
public class Child extends Parent {
    static {
        System.out.println("Child的靜態塊執行了!");
    }

    public Child() {
        System.out.println("Child的構造方法執行了!");
    }

    {
        System.out.println("Child的程式碼塊被執行了!");
    }
}

public static void main( String[] args ) {
    Child child = new Child();
    System.out.println();
}

//輸出結果
Parent的靜態塊執行了!
Child的靜態塊執行了!
Parent的程式碼塊被執行了!
Parent的構造方法執行了!
Child的程式碼塊被執行了!
Child的構造方法執行了!

相關文章