【轉(zhuǎn)】Java類的初始化與實例化

摘要:

?在Java中,一個對象在可以被使用之前必須要被正確地初始化,這一點是Java規(guī)范規(guī)定的。在實例化一個對象時,JVM首先會檢查相關(guān)類型是否已經(jīng)加載并初始化,如果沒有,則JVM立即進行加載并調(diào)用類構(gòu)造器完成類的初始化。在類初始化過程中或初始化完畢后,根據(jù)具體情況才會去對類進行實例化。本文試圖對JVM執(zhí)行類初始化和實例化的過程做一個詳細深入地介紹,以便從Java虛擬機的角度清晰解剖一個Java對象的創(chuàng)建過程。


友情提示:

?一個Java對象的創(chuàng)建過程往往包括類初始化類實例化 兩個階段。在《JVM類加載機制》主要介紹了類的初始化時機和初始化過程,本文在此基礎(chǔ)上,進一步闡述了一個Java對象創(chuàng)建的真實過程。

一、Java對象創(chuàng)建時機

?我們知道,一個對象在可以被使用之前必須要被正確地實例化。在Java代碼中,有很多行為可以引起對象的創(chuàng)建,最為直觀的一種就是使用new關(guān)鍵字來調(diào)用一個類的構(gòu)造函數(shù)顯式地創(chuàng)建對象,這種方式在Java規(guī)范中被稱為 : 由執(zhí)行類實例創(chuàng)建表達式而引起的對象創(chuàng)建。除此之外,我們還可以使用反射機制(Class類的newInstance方法、使用Constructor類的newInstance方法)、使用Clone方法、使用反序列化等方式創(chuàng)建對象。下面筆者分別對此進行一一介紹:
1). 使用new關(guān)鍵字創(chuàng)建對象

?這是我們最常見的也是最簡單的創(chuàng)建對象的方式,通過這種方式我們可以調(diào)用任意的構(gòu)造函數(shù)(無參的和有參的)去創(chuàng)建對象。比如:

  Student student = new Student();

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

?我們也可以通過Java的反射機制使用Class類的newInstance方法來創(chuàng)建對象,事實上,這個newInstance方法調(diào)用無參的構(gòu)造器創(chuàng)建對象,比如:

 Student student2 = (Student)Class.forName("Student類全限定名").newInstance(); 
或者:
  Student stu = Student.class.newInstance();

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

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

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方法的這兩種方式創(chuàng)建對象使用的就是Java的反射機制,事實上Class的newInstance方法內(nèi)部調(diào)用的也是Constructor的newInstance方法。

4). 使用Clone方法創(chuàng)建對象

?無論何時我們調(diào)用一個對象的clone方法,JVM都會幫我們創(chuàng)建一個新的、一樣的對象,特別需要說明的是,用clone方法創(chuàng)建對象的過程中并不會調(diào)用任何構(gòu)造函數(shù)。簡單而言,要想使用clone方法,我們就必須先實現(xiàn)Cloneable接口并實現(xiàn)其定義的clone方法,這也是原型模式的應(yīng)用。比如:

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). 使用(反)序列化機制創(chuàng)建對象

?當我們反序列化一個對象時,JVM會給我們創(chuàng)建一個單獨的對象,在此過程中,JVM并不會調(diào)用任何構(gòu)造函數(shù)。為了反序列化一個對象,我們需要讓我們的類實現(xiàn)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關(guān)鍵字創(chuàng)建對象:");
        Student stu1 = new Student(123);
        System.out.println(stu1);
        System.out.println("\n---------------------------\n");


        System.out.println("使用Class類的newInstance方法創(chuàng)建對象:");
        Student stu2 = Student.class.newInstance();    //對應(yīng)類必須具有無參構(gòu)造方法,且只有這一種創(chuàng)建方式
        System.out.println(stu2);
        System.out.println("\n---------------------------\n");

        System.out.println("使用Constructor類的newInstance方法創(chuàng)建對象:");
        Constructor<Student> constructor = Student.class
                .getConstructor(Integer.class);   // 調(diào)用有參構(gòu)造方法
        Student stu3 = constructor.newInstance(123);   
        System.out.println(stu3);
        System.out.println("\n---------------------------\n");

        System.out.println("使用Clone方法創(chuàng)建對象:");
        Student stu4 = (Student) stu3.clone();
        System.out.println(stu4);
        System.out.println("\n---------------------------\n");

        System.out.println("使用(反)序列化機制創(chuàng)建對象:");
        // 寫對象
        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關(guān)鍵字創(chuàng)建對象:
        Student [id=123]

        ---------------------------

        使用Class類的newInstance方法創(chuàng)建對象:
        Student [id=0]

        ---------------------------

        使用Constructor類的newInstance方法創(chuàng)建對象:
        Student [id=123]

        ---------------------------

        使用Clone方法創(chuàng)建對象:
        Student [id=123]

        ---------------------------

        使用(反)序列化機制創(chuàng)建對象:
        Student [id=123]
*///:~

?從Java虛擬機層面看,除了使用new關(guān)鍵字創(chuàng)建對象的方式外,其他方式全部都是通過轉(zhuǎn)變?yōu)閕nvokevirtual指令直接創(chuàng)建對象的。


二. Java 對象的創(chuàng)建過程

?當一個對象被創(chuàng)建時,虛擬機就會為其分配內(nèi)存來存放對象自己的實例變量及其從父類繼承過來的實例變量(即使這些從超類繼承過來的實例變量有可能被隱藏也會被分配空間)。在為這些實例變量分配內(nèi)存的同時,這些實例變量也會被賦予默認值(零值)。在內(nèi)存分配完成之后,Java虛擬機就會開始對新創(chuàng)建的對象按照程序員的意志進行初始化。在Java對象初始化過程中,主要涉及三種執(zhí)行對象初始化的結(jié)構(gòu),分別是 實例變量初始化、實例代碼塊初始化 以及 構(gòu)造函數(shù)初始化。

1、實例變量初始化與實例代碼塊初始化

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

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

?上面的例子正好印證了上面的結(jié)論。特別需要注意的是,Java是按照編程順序來執(zhí)行實例變量初始化器和實例初始化器中的代碼的,并且不允許順序靠前的實例代碼塊初始化在其后面定義的實例變量,比如:

public class InstanceInitializer {  
    {  
        j = i;  
    }  

    private int i = 1;  
    private int j;  
}  

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

?上面的這些代碼都是無法通過編譯的,編譯器會抱怨說我們使用了一個未經(jīng)定義的變量。之所以要這么做是為了保證一個變量在被使用之前已經(jīng)被正確地初始化。但是我們?nèi)匀挥修k法繞過這種檢查,比如:

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);  
    }  
}  

?如果我們執(zhí)行上面這段代碼,那么會發(fā)現(xiàn)打印的結(jié)果是0。因此我們可以確信,變量j被賦予了i的默認值0,這一動作發(fā)生在實例變量i初始化之前和構(gòu)造函數(shù)調(diào)用之前。

2、構(gòu)造函數(shù)初始化

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

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

public class ConstructorExample {  

} 

?對于上面代碼中定義的類,我們觀察編譯之后的字節(jié)碼,我們會發(fā)現(xiàn)編譯器為我們生成一個構(gòu)造函數(shù),如下,

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

?上面代碼的第二行就是調(diào)用Object類的默認構(gòu)造函數(shù)的指令。也就是說,如果我們顯式調(diào)用超類的構(gòu)造函數(shù),那么該調(diào)用必須放在構(gòu)造函數(shù)所有代碼的最前面,也就是必須是構(gòu)造函數(shù)的第一條指令。正因為如此,Java才可以使得一個對象在初始化之前其所有的超類都被初始化完成,并保證創(chuàng)建一個完整的對象出來。

?特別地,如果我們在一個構(gòu)造函數(shù)中調(diào)用另外一個構(gòu)造函數(shù),如下所示,

public class ConstructorExample {  
    private int i;  

    ConstructorExample() {  
        this(1);  
        ....  
    }  

    ConstructorExample(int i) {  
        ....  
        this.i = i;  
        ....  
    }  
}  

?對于這種情況,Java只允許在ConstructorExample(int i)內(nèi)調(diào)用超類的構(gòu)造函數(shù),也就是說,下面兩種情形的代碼編譯是無法通過的:

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;  
    }  
}   

3、 小結(jié)

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

?在準備實例化一個類的對象前,首先準備實例化該類的父類,如果該類的父類還有父類,那么準備實例化該類的父類的父類,依次遞歸直到遞歸到Object類。此時,首先實例化Object類,再依次對以下各類進行實例化,直到完成對目標類的實例化。具體而言,在實例化每個類時,都遵循如下順序:先依次執(zhí)行實例變量初始化和實例代碼塊初始化,再執(zhí)行構(gòu)造函數(shù)初始化。也就是說,編譯器會將實例變量初始化和實例代碼塊初始化相關(guān)代碼放到類的構(gòu)造函數(shù)中去,并且這些代碼會被放在對超類構(gòu)造函數(shù)的調(diào)用語句之后,構(gòu)造函數(shù)本身的代碼之前。

Java類實例化遞歸過程

4、實例變量初始化、實例代碼塊初始化以及構(gòu)造函數(shù)初始化綜合實例

在《JVM類加載機制》一文中詳細闡述了類初始化時機和初始化過程,并在文章的最后留了一個懸念給各位,這里來揭開這個懸念。

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

?根據(jù)上文所述的類實例化過程,我們可以將Foo類的構(gòu)造函數(shù)和Bar類的構(gòu)造函數(shù)等價地分別變?yōu)槿缦滦问剑?/p>

  //Foo類構(gòu)造函數(shù)的等價變換:
    Foo() {
        i = 1;
        i = 2;
        System.out.println(i);
        int x = getValue();
        System.out.println(x);
    }

 //Bar類構(gòu)造函數(shù)的等價變換
    Bar() {
        Foo();
        j = 1;
        j = 3;
        j = 2
    }

?這樣程序就好看多了,我們一眼就可以觀察出程序的輸出結(jié)果。在通過使用Bar類的構(gòu)造方法new一個Bar類的實例時,首先會調(diào)用Foo類構(gòu)造函數(shù),因此(1)處輸出是2,這從Foo類構(gòu)造函數(shù)的等價變換中可以直接看出。(2)處輸出是0,為什么呢?因為在執(zhí)行Foo的構(gòu)造函數(shù)的過程中,由于Bar重載了Foo中的getValue方法,所以根據(jù)Java的多態(tài)特性可以知道,其調(diào)用的getValue方法是被Bar重載的那個getValue方法。但由于這時Bar的構(gòu)造函數(shù)還沒有被執(zhí)行,因此此時j的值還是默認值0,因此(2)處輸出是0。最后,在執(zhí)行(3)處的代碼時,由于bar對象已經(jīng)創(chuàng)建完成,所以此時再訪問j的值時,就得到了其初始化后的值2,這一點可以從Bar類構(gòu)造函數(shù)的等價變換中直接看出。


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

?關(guān)于類的初始化時機,筆者在博文《 JVM類加載機制概述:加載時機與加載過程》已經(jīng)介紹的很清楚了,此處不再贅述。簡單地說,在類加載過程中,準備階段是正式為類變量(static 成員變量)分配內(nèi)存并設(shè)置類變量初始值(零值)的階段,而初始化階段是真正開始執(zhí)行類中定義的java程序代碼(字節(jié)碼)并按程序猿的意圖去初始化類變量的過程。更直接地說,初始化階段就是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)代碼塊static{}中的語句合并產(chǎn)生的,其中編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定。

?類構(gòu)造器<clinit>()與實例構(gòu)造器<init>()不同,它不需要程序員進行顯式調(diào)用,虛擬機會保證在子類類構(gòu)造器<clinit>()執(zhí)行之前,父類的類構(gòu)造<clinit>()執(zhí)行完畢。由于父類的構(gòu)造器<clinit>()先執(zhí)行,也就意味著父類中定義的靜態(tài)代碼塊/靜態(tài)變量的初始化要優(yōu)先于子類的靜態(tài)代碼塊/靜態(tài)變量的初始化執(zhí)行。特別地,類構(gòu)造器<clinit>()對于類或者接口來說并不是必需的,如果一個類中沒有靜態(tài)代碼塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生產(chǎn)類構(gòu)造器<clinit>()。此外,在同一個類加載器下,一個類只會被初始化一次,但是一個類可以任意地實例化對象。也就是說,在一個類的生命周期中,類構(gòu)造器<clinit>()最多會被虛擬機調(diào)用一次,而實例構(gòu)造器<init>()則會被虛擬機調(diào)用多次,只要程序員還在創(chuàng)建對象。

?注意,這里所謂的實例構(gòu)造器<init>()是指收集類中的所有實例變量的賦值動作、實例代碼塊和構(gòu)造函數(shù)合并產(chǎn)生的,類似于上文對Foo類的構(gòu)造函數(shù)和Bar類的構(gòu)造函數(shù)做的等價變換。

四. 總結(jié)

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

?我們知道,JVM在為一個對象分配完內(nèi)存之后,會給每一個實例變量賦予默認值,這個時候?qū)嵗兞勘坏谝淮钨x值,這個賦值過程是沒有辦法避免的。如果我們在聲明實例變量x的同時對其進行了賦值操作,那么這個時候,這個實例變量就被第二次賦值了。如果我們在實例代碼塊中,又對變量x做了初始化操作,那么這個時候,這個實例變量就被第三次賦值了。如果我們在構(gòu)造函數(shù)中,也對變量x做了初始化操作,那么這個時候,變量x就被第四次賦值。也就是說,在Java的對象初始化過程中,一個實例變量最多可以被初始化4次。

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

?類的初始化是指類加載過程中的初始化階段對類變量按照程序猿的意圖進行賦值的過程;而類的實例化是指在類完全加載到內(nèi)存中后創(chuàng)建對象的過程。

3、假如一個類還未加載到內(nèi)存中,那么在創(chuàng)建一個該類的實例時,具體過程是怎樣的?

?我們知道,要想創(chuàng)建一個類的實例,必須先將該類加載到內(nèi)存并進行初始化,也就是說,類初始化操作是在類實例化操作之前進行的,但并不意味著:只有類初始化操作結(jié)束后才能進行類實例化操作。例如,筆者在博文《 JVM類加載機制概述:加載時機與加載過程》中所提到的下面這個經(jīng)典案例:

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

    static StaticTest st = new StaticTest();

    static {   //靜態(tài)代碼塊
        System.out.println("1");
    }

    {       // 實例代碼塊
        System.out.println("2");
    }

    StaticTest() {    // 實例構(gòu)造器
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

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

    int a = 110;    // 實例變量
    static int b = 112;     // 靜態(tài)變量
}/* Output: 
        2
        3
        a=110,b=0
        1
        4
 *///:~

總的來說,類實例化的一般過程是:父類的類構(gòu)造器<clinit>() -> 子類的類構(gòu)造器<clinit>() -> 父類的成員變量和實例代碼塊 -> 父類的構(gòu)造函數(shù) -> 子類的成員變量和實例代碼塊 -> 子類的構(gòu)造函數(shù)。



參考引用

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容