深入學習Java對象創建的過程:類的初始化與實例化

  • 2019 年 10 月 3 日
  • 筆記

  在Java中,一個對象在可以被使用之前必須要被正確地初始化,這一點是Java規範規定的。在實例化一個對象時,JVM首先會檢查相關類型是否已經載入並初始化,如果沒有,則JVM立即進行載入並調用類構造器完成類的初始化。在類初始化過程中或初始化完畢後,根據具體情況才會去對類進行實例化。本文試圖對JVM執行類初始化和實例化的過程做一個詳細深入地介紹,以便從Java虛擬機的角度清晰解剖一個Java對象的創建過程。
一個Java對象的創建過程往往包括 類初始化 和 類實例化 兩個階段。本文的是在上一篇《JVM類生命周期概述:載入時機與載入過程》主要介紹了類的初始化時機和初始化過程,本文在此基礎上,進一步闡述了一個Java對象創建的真實過程。


 

一、Java對象創建時機  

我們知道,一個對象在可以被使用之前必須要被正確地實例化。在Java程式碼中,有很多行為可以引起對象的創建,最為直觀的一種就是使用new關鍵字來調用一個類的構造函數顯式地創建對象,這種方式在Java規範中被稱為 : 由執行類實例創建表達式而引起的對象創建。除此之外,我們還可以使用反射機制(Class類的newInstance方法、使用Constructor類的newInstance方法)、使用Clone方法、使用反序列化等方式創建對象。下面本文分別對此進行一一介紹: 

1). 使用new關鍵字創建對象

  這是我們最常見的也是最簡單的創建對象的方式,通過這種方式我們可以調用任意的構造函數(無參的和有參的)去創建對象。比如:

People people= new People();

2). 使用Class類的newInstance方法(反射機制)

  我們也可以通過Java的反射機制使用Class類的newInstance方法來創建對象,事實上,這個newInstance方法調用無參的構造器創建對象,比如:

People p= (People )Class.forName("People類全限定名").newInstance();  或者:   People p=People .class.newInstance();

3).使用Constructor類的newInstance方法(反射機制)

  java.lang.relect.Constructor類里也有一個newInstance方法可以創建對象,該方法和Class類中的newInstance方法很像,但是相比之下,Constructor類的newInstance方法更加強大些,我們可以通過這個newInstance方法調用有參數的和私有的構造函數,比如:

public class Student {        private int id;        public Student(Integer id) {          this.id = id;      }        public static void main(String[] args) throws Exception {            Constructor<Student> constructor = Student.class                  .getConstructor(Integer.class);          Student stu3 = constructor.newInstance(123);      }  }

 使用newInstance方法的這兩種方式創建對象使用的就是Java的反射機制,事實上Class的newInstance方法內部調用的也是Constructor的newInstance方法。

4). 使用Clone方法創建對象

  無論何時我們調用一個對象的clone方法,JVM都會幫我們創建一個新的、一樣的對象,特別需要說明的是,用clone方法創建對象的過程中並不會調用任何構造函數。所以,要想使用clone方法,我們就必須先實現Cloneable介面並實現其定義的clone方法,這也是原型模式的應用。比如:

public class Student implements Cloneable{        private int id;        public Student(Integer id) {          this.id = id;      }        @Override      protected Object clone() throws CloneNotSupportedException {          // TODO Auto-generated method stub          return super.clone();      }        public static void main(String[] args) throws Exception {            Constructor<Student> constructor = Student.class                  .getConstructor(Integer.class);          Student stu3 = constructor.newInstance(123);          Student stu4 = (Student) stu3.clone();      }  }

5). 使用(反)序列化機制創建對象

  當我們反序列化一個對象時,JVM會給我們創建一個單獨的對象,在此過程中,JVM並不會調用任何構造函數。為了反序列化一個對象,我們需要讓我們的類實現Serializable介面,比如:

public class Student implements Cloneable, Serializable {        private int id;        public Student(Integer id) {          this.id = id;      }        @Override      public String toString() {          return "Student [id=" + id + "]";      }        public static void main(String[] args) throws Exception {            Constructor<Student> constructor = Student.class                  .getConstructor(Integer.class);          Student stu3 = constructor.newInstance(123);            // 寫對象          ObjectOutputStream output = new ObjectOutputStream(                  new FileOutputStream("student.bin"));          output.writeObject(stu3);          output.close();            // 讀對象          ObjectInputStream input = new ObjectInputStream(new FileInputStream(                  "student.bin"));          Student stu5 = (Student) input.readObject();          System.out.println(stu5);      }  }

6). 完整實例

public class Student implements Cloneable, Serializable {        private int id;        public Student() {        }        public Student(Integer id) {          this.id = id;      }        @Override      protected Object clone() throws CloneNotSupportedException {          // TODO Auto-generated method stub          return super.clone();      }        @Override      public String toString() {          return "Student [id=" + id + "]";      }        public static void main(String[] args) throws Exception {            System.out.println("使用new關鍵字創建對象:");          Student stu1 = new Student(123);          System.out.println(stu1);          System.out.println("n---------------------------n");              System.out.println("使用Class類的newInstance方法創建對象:");          Student stu2 = Student.class.newInstance();    //對應類必須具有無參構造方法,且只有這一種創建方式          System.out.println(stu2);          System.out.println("n---------------------------n");            System.out.println("使用Constructor類的newInstance方法創建對象:");          Constructor<Student> constructor = Student.class                  .getConstructor(Integer.class);   // 調用有參構造方法          Student stu3 = constructor.newInstance(123);          System.out.println(stu3);          System.out.println("n---------------------------n");            System.out.println("使用Clone方法創建對象:");          Student stu4 = (Student) stu3.clone();          System.out.println(stu4);          System.out.println("n---------------------------n");            System.out.println("使用(反)序列化機制創建對象:");          // 寫對象          ObjectOutputStream output = new ObjectOutputStream(                  new FileOutputStream("student.bin"));          output.writeObject(stu4);          output.close();            // 讀取對象          ObjectInputStream input = new ObjectInputStream(new FileInputStream(                  "student.bin"));          Student stu5 = (Student) input.readObject();          System.out.println(stu5);        }  }/* Output:          使用new關鍵字創建對象:          Student [id=123]            ---------------------------            使用Class類的newInstance方法創建對象:          Student [id=0]            ---------------------------            使用Constructor類的newInstance方法創建對象:          Student [id=123]            ---------------------------            使用Clone方法創建對象:          Student [id=123]            ---------------------------            使用(反)序列化機制創建對象:          Student [id=123]  *///:~

View Code

 從Java虛擬機層面看,除了使用new關鍵字創建對象的方式外,其他方式全部都是通過轉變為invokevirtual指令直接創建對象的。


 

二. Java 對象的創建過程 

 當一個對象被創建時,虛擬機就會為其分配記憶體來存放對象自己的實例變數及其從父類繼承過來的實例變數(即使這些從超類繼承過來的實例變數有可能被隱藏也會被分配空間)。在為這些實例變數分配記憶體的同時,這些實例變數也會被賦予默認值(零值)。在記憶體分配完成之後,Java虛擬機就會開始對新創建的對象按照程式猿的意志進行初始化。在Java對象初始化過程中,主要涉及三種執行對象初始化的結構,分別是 實例變數初始化、實例程式碼塊初始化 以及 構造函數初始化。

1、實例變數初始化與實例程式碼塊初始化

  我們在定義(聲明)實例變數的同時,還可以直接對實例變數進行賦值或者使用實例程式碼塊對其進行賦值。如果我們以這兩種方式為實例變數進行初始化,那麼它們將在構造函數執行之前完成這些初始化操作。實際上,如果我們對實例變數直接賦值或者使用實例程式碼塊賦值,那麼編譯器會將其中的程式碼放到類的構造函數中去,並且這些程式碼會被放在對超類構造函數的調用語句之後(還記得嗎?Java要求構造函數的第一條語句必須是超類構造函數的調用語句),構造函數本身的程式碼之前。例如:

public class InstanceVariableInitializer {        private int i = 1;      private int j = i + 1;        public InstanceVariableInitializer(int var){          System.out.println(i);          System.out.println(j);          this.i = var;          System.out.println(i);          System.out.println(j);      }        {               // 實例程式碼塊          j += 3;        }        public static void main(String[] args) {          new InstanceVariableInitializer(8);      }  }/* Output:              1              5              8              5   */

上面的例子正好印證了上面的結論。特別需要注意的是,Java是按照編程順序來執行實例變數初始化器和實例初始化器中的程式碼的,並且不允許順序靠前的實例程式碼塊初始化在其後面定義的實例變數,比如:

public class InstanceInitializer {      {          j = i;      }        private int i = 1;      private int j;  }    public class InstanceInitializer {      private int j = i;      private int i = 1;  }  

上面的這些程式碼都是無法通過編譯的,編譯器會抱怨說我們使用了一個未經定義的變數。之所以要這麼做是為了保證一個變數在被使用之前已經被正確地初始化。但是我們仍然有辦法繞過這種檢查,比如:

public class InstanceInitializer {      private int j = getI();      private int i = 1;        public InstanceInitializer() {          i = 2;      }        private int getI() {          return i;      }        public static void main(String[] args) {          InstanceInitializer ii = new InstanceInitializer();          System.out.println(ii.j);      }  }  

如果我們執行上面這段程式碼,那麼會發現列印的結果是0。因此我們可以確信,變數j被賦予了i的默認值0,這一動作發生在實例變數i初始化之前和構造函數調用之前。

2、構造函數初始化

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

  我們知道,Java要求在實例化類之前,必須先實例化其超類,以保證所創建實例的完整性。事實上,這一點是在構造函數中保證的:Java強制要求Object對象(Object是Java的頂層對象,沒有超類)之外的所有對象構造函數的第一條語句必須是超類構造函數的調用語句或者是類中定義的其他的構造函數,如果我們既沒有調用其他的構造函數,也沒有顯式調用超類的構造函數,那麼編譯器會為我們自動生成一個對超類構造函數的調用,比如:

public class ConstructorExample {    } 

對於上面程式碼中定義的類,我們觀察編譯之後的位元組碼,我們會發現編譯器為我們生成一個構造函數,如下,

aload_0  invokespecial   #8; //Method java/lang/Object."<init>":()V    return 

 上面程式碼的第二行就是調用Object類的默認構造函數的指令。也就是說,如果我們顯式調用超類的構造函數,那麼該調用必須放在構造函數所有程式碼的最前面,也就是必須是構造函數的第一條指令。正因為如此,Java才可以使得一個對象在初始化之前其所有的超類都被初始化完成,並保證創建一個完整的對象出來。
特別地,如果我們在一個構造函數中調用另外一個構造函數,如下所示,

public class ConstructorExample {      private int i;        ConstructorExample() {          this(1);          ....      }        ConstructorExample(int i) {          ....          this.i = i;          ....      }  }  

對於這種情況,Java只允許在ConstructorExample(int i)內調用超類的構造函數,也就是說,下面兩種情形的程式碼編譯是無法通過的:

public class ConstructorExample {      private int i;        ConstructorExample() {          super();          this(1);  // Error:Constructor call must be the first statement in a constructor          ....      }        ConstructorExample(int i) {          ....          this.i = i;          ....      }  }  

或者:

public class ConstructorExample {      private int i;        ConstructorExample() {          this(1);          super();  //Error: Constructor call must be the first statement in a constructor          ....      }        ConstructorExample(int i) {          this.i = i;      }  }   

Java通過對構造函數作出這種限制以便保證一個類的實例能夠在被使用之前正確地初始化。

  總而言之,實例化一個類的對象的過程是一個典型的遞歸過程,如下圖所示。進一步地說,在實例化一個類的對象時,具體過程是這樣的:

  在準備實例化一個類的對象前,首先準備實例化該類的父類,如果該類的父類還有父類,那麼準備實例化該類的父類的父類,依次遞歸直到遞歸到Object類。此時,首先實例化Object類,再依次對以下各類進行實例化,直到完成對目標類的實例化。具體而言,在實例化每個類時,都遵循如下順序:先依次執行實例變數初始化和實例程式碼塊初始化,再執行構造函數初始化。也就是說,編譯器會將實例變數初始化和實例程式碼塊初始化相關程式碼放到類的構造函數中去,並且這些程式碼會被放在對超類構造函數的調用語句之後,構造函數本身的程式碼之前。

三. 類的初始化時機與過程 

 關於類的初始化時機,在博文《 JVM類載入機制概述:載入時機與載入過程》已經介紹的很清楚了,此處不再贅述。簡單地說,在類載入過程中,準備階段是正式為類變數(static 成員變數)分配記憶體並設置類變數初始值(零值)的階段,而初始化階段是真正開始執行類中定義的java程式程式碼(位元組碼)並按程式猿的意圖去初始化類變數的過程。更直接地說,初始化階段就是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊static{}中的語句合併產生的,其中編譯器收集的順序是由語句在源文件中出現的順序所決定。

  類構造器<clinit>()與實例構造器<init>()不同,它不需要程式設計師進行顯式調用,虛擬機會保證在子類類構造器<clinit>()執行之前,父類的類構造<clinit>()執行完畢。由於父類的構造器<clinit>()先執行,也就意味著父類中定義的靜態程式碼塊/靜態變數的初始化要優先於子類的靜態程式碼塊/靜態變數的初始化執行。特別地,類構造器<clinit>()對於類或者介面來說並不是必需的,如果一個類中沒有靜態程式碼塊,也沒有對類變數的賦值操作,那麼編譯器可以不為這個類生產類構造器<clinit>()。此外,在同一個類載入器下,一個類只會被初始化一次,但是一個類可以任意地實例化對象。也就是說,在一個類的生命周期中,類構造器<clinit>()最多會被虛擬機調用一次,而實例構造器<init>()則會被虛擬機調用多次,只要程式設計師還在創建對象。

  注意,這裡所謂的實例構造器<init>()是指收集類中的所有實例變數的賦值動作、實例程式碼塊和構造函數合併產生的,類似於上文對Foo類的構造函數和Bar類的構造函數做的等價變換。


思考:

1、一個實例變數在對象初始化的過程中會被賦值幾次?

  我們知道,JVM在為一個對象分配完記憶體之後,會給每一個實例變數賦予默認值,這個時候實例變數被第一次賦值,這個賦值過程是沒有辦法避免的。如果我們在聲明實例變數x的同時對其進行了賦值操作,那麼這個時候,這個實例變數就被第二次賦值了。如果我們在實例程式碼塊中,又對變數x做了初始化操作,那麼這個時候,這個實例變數就被第三次賦值了。如果我們在構造函數中,也對變數x做了初始化操作,那麼這個時候,變數x就被第四次賦值。也就是說,在Java的對象初始化過程中,一個實例變數最多可以被初始化4次。

2、類的初始化過程與類的實例化過程的異同?

 類的初始化是指類載入過程中的初始化階段對類變數按照程式猿的意圖進行賦值的過程;而類的實例化是指在類完全載入到記憶體中後創建對象的過程。

3、假如一個類還未載入到記憶體中,那麼在創建一個該類的實例時,具體過程是怎樣的?

 我們知道,要想創建一個類的實例,必須先將該類載入到記憶體並進行初始化,也就是說,類初始化操作是在類實例化操作之前進行的,但並不意味著:只有類初始化操作結束後才能進行類實例化操作。
總的來說,類實例化的一般過程是:父類的類構造器<clinit>() -> 子類的類構造器<clinit>() -> 父類的成員變數和實例程式碼塊 -> 父類的構造函數 -> 子類的成員變數和實例程式碼塊 -> 子類的構造函數。