歡迎光臨
每天分享高質量文章

深入理解Java物件的建立過程:類的初始化與實體化

作者:書獃子Rico 
部落格地址:http://blog.csdn.net/justloveyou_/

連結:https://blog.csdn.net/justloveyou_/article/details/72466416

摘要:

在Java中,一個物件在可以被使用之前必須要被正確地初始化,這一點是Java規範規定的。在實體化一個物件時,JVM首先會檢查相關型別是否已經載入並初始化,如果沒有,則JVM立即進行載入並呼叫類建構式完成類的初始化。在類初始化過程中或初始化完畢後,根據具體情況才會去對類進行實體化。本文試圖對JVM執行類初始化和實體化的過程做一個詳細深入地介紹,以便從Java虛擬機器的角度清晰解剖一個Java物件的建立過程。

一、Java物件建立時機

我們知道,一個物件在可以被使用之前必須要被正確地實體化。在Java程式碼中,有很多行為可以引起物件的建立,最為直觀的一種就是使用new關鍵字來呼叫一個類的建構式顯式地建立物件,這種方式在Java規範中被稱為 : 由執行類實體建立運算式而引起的物件建立。除此之外,我們還可以使用反射機制(Class類的newInstance方法、使用Constructor類的newInstance方法)、使用Clone方法、使用反序列化等方式建立物件。下麵筆者分別對此進行一一介紹:


1). 使用new關鍵字建立物件

這是我們最常見的也是最簡單的建立物件的方式,透過這種方式我們可以呼叫任意的建構式(無參的和有參的)去建立物件。比如:

Student student = new Student();

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

我們也可以透過Java的反射機制使用Class類的newInstance方法來建立物件,事實上,這個newInstance方法呼叫無參的建構式建立物件,比如:

Student student2 = (Student)Class.forName("Student類全限定名").newInstance(); 
或者:
Student stu = Student.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 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 constructor = Student.class
               .getConstructor(Integer.class);
       Student stu3 = constructor.newInstance(123);
       Student stu4 = (Student) stu3.clone();
   }
}

5). 使用(反)序列化機制建立物件

當我們反序列化一個物件時,JVM會給我們建立一個單獨的物件,在此過程中,JVM並不會呼叫任何建構式。為了反序列化一個物件,我們需要讓我們的類實現Serializable介面,比如:

public class Student implements CloneableSerializable {

   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 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 CloneableSerializable {

   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(" --------------------------- ");


       System.out.println("使用Class類的newInstance方法建立物件:");
       Student stu2 = Student.class.newInstance();    //對應類必須具有無參構造方法,且只有這一種建立方式
       System.out.println(stu2);
       System.out.println(" --------------------------- ");

       System.out.println("使用Constructor類的newInstance方法建立物件:");
       Constructor constructor = Student.class
               .getConstructor(Integer.class);   // 呼叫有參構造方法
       Student stu3 = constructor.newInstance(123);   
       System.out.println(stu3);
       System.out.println(" --------------------------- ");

       System.out.println("使用Clone方法建立物件:");
       Student stu4 = (Student) stu3.clone();
       System.out.println(stu4);
       System.out.println(" --------------------------- ");

       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]
*/
//:~

  

從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中的物件都至少會有一個建構式,如果我們沒有顯式定義建構式,那麼它將會有一個預設無參的建構式。在編譯生成的位元組碼中,這些建構式會被命名成()方法,引數串列與Java語言書寫的建構式的引數串列相同。

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

public class ConstructorExample {  

}

  

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

aload_0  
invokespecial   #8; //Method java/lang/Object."":()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透過對建構式作出這種限制以便保證一個類的實體能夠在被使用之前正確地初始化。


3、 小結

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

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


4、實體變數初始化、實體程式碼塊初始化以及建構式初始化綜合實體

//父類
class Foo {
   int i = 1;

   Foo() {
       System.out.println(i);             -----------(1)
       int x = getValue();
       System.out.println(x);             -----------(2)
   }

   {
       i = 2;
   }

   protected int getValue({
       return i;
   }
}

//子類
class Bar extends Foo {
   int j = 1;

   Bar() {
       j = 2;
   }

   {
       j = 3;
   }

   @Override
   protected int getValue({
       return j;
   }
}

public class ConstructorExample {
   public static void main(String... args{
       Bar bar = new Bar();
       System.out.println(bar.getValue());             -----------(3)
   }
}/* Output: 
           2
           0
           2
*/
//:~

  

根據上文所述的類實體化過程,我們可以將Foo類的建構式和Bar類的建構式等價地分別變為如下形式:

//Foo類建構式的等價變換:
   Foo() {
       i = 1;
       i = 2;
       System.out.println(i);
       int x = getValue();
       System.out.println(x);
   }

  

//Bar類建構式的等價變換
   Bar() {
       Foo();
       j = 1;
       j = 3;
       j = 2
   }


這樣程式就好看多了,我們一眼就可以觀察出程式的輸出結果。在透過使用Bar類的構造方法new一個Bar類的實體時,首先會呼叫Foo類建構式,因此(1)處輸出是2,這從Foo類建構式的等價變換中可以直接看出。(2)處輸出是0,為什麼呢?因為在執行Foo的建構式的過程中,由於Bar多載了Foo中的getValue方法,所以根據Java的多型特性可以知道,其呼叫的getValue方法是被Bar多載的那個getValue方法。但由於這時Bar的建構式還沒有被執行,因此此時j的值還是預設值0,因此(2)處輸出是0。最後,在執行(3)處的程式碼時,由於bar物件已經建立完成,所以此時再訪問j的值時,就得到了其初始化後的值2,這一點可以從Bar類建構式的等價變換中直接看出。


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

簡單地說,在類載入過程中,準備階段是正式為類變數(static 成員變數)分配記憶體並設定類變數初始值(零值)的階段,而初始化階段是真正開始執行類中定義的java程式程式碼(位元組碼)並按程式猿的意圖去初始化類變數的過程。更直接地說,初始化階段就是執行類建構式()方法的過程。()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊static{}中的陳述句合併產生的,其中編譯器收集的順序是由陳述句在源檔案中出現的順序所決定。

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

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


四. 總結

1、一個實體變數在物件初始化的過程中會被賦值幾次?

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


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

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


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

我們知道,要想建立一個類的實體,必須先將該類載入到記憶體併進行初始化,也就是說,類初始化操作是在類實體化操作之前進行的,但並不意味著:只有類初始化操作結束後才能進行類實體化操作。例如下麵這個經典案例:

public class StaticTest {
   public static void main(String[] args{
       staticFunction();
   }

   static StaticTest st = new StaticTest();

   static {   //靜態程式碼塊
       System.out.println("1");
   }

   {       // 實體程式碼塊
       System.out.println("2");
   }

   StaticTest() {    // 實體建構式
       System.out.println("3");
       System.out.println("a=" + a + ",b=" + b);
   }

   public static void staticFunction({   // 靜態方法
       System.out.println("4");
   }

   int a = 110;    // 實體變數
   static int b = 112;     // 靜態變數
}/* Output: 
       2
       3
       a=110,b=0
       1
       4
*/
//:~

  

大家能得到正確答案嗎?

總的來說,類實體化的一般過程是:父類的類建構式() -> 子類的類建構式() -> 父類的成員變數和實體程式碼塊 -> 父類的建構式 -> 子類的成員變數和實體程式碼塊 -> 子類的建構式。



編號764,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

贊(0)

分享創造快樂