Java對象的建立:類的初始化時機與過程

來源:互聯網
上載者:User
摘要:

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

從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通過對建構函式作出這種限制以便保證一個類的執行個體能夠在被使用之前正確地初始化。

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程式碼(位元組碼)並按程式猿的意圖去初始化類變數的過程。更直接地說,初始化階段就是執行類構造器<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、假如一個類還未載入到記憶體中,那麼在建立一個該類的執行個體時,具體過程是怎樣的?

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

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

總的來說,類執行個體化的一般過程是:父類的類構造器<clinit>() -> 子類的類構造器<clinit>() -> 父類的成員變數和執行個體代碼塊 -> 父類的建構函式 -> 子類的成員變數和執行個體代碼塊 -> 子類的建構函式。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.