咱們知道,一個對象在能夠被使用以前必需要被正確地實例化。在Java代碼中,有不少行爲能夠引發對象的建立,最爲直觀的一種就是使用new關鍵字來調用一個類的構造函數顯式地建立對象,這種方式在Java規範中被稱爲 : 由執行類實例建立表達式而引發的對象建立。除此以外,咱們還可使用反射機制(Class類的newInstance方法、使用Constructor類的newInstance方法)、使用Clone方法、使用反序列化等方式建立對象。java
1). 使用new關鍵字建立對象程序員
這是咱們最多見的也是最簡單的建立對象的方式,經過這種方式咱們能夠調用任意的構造函數(無參的和有參的)去建立對象。好比:算法
Student student = new Student();
2). 使用Class類的newInstance方法(反射機制)編程
咱們也能夠經過Java的反射機制使用Class類的newInstance方法來建立對象,事實上,這個newInstance方法調用無參的構造器建立對象,好比:ide
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方法調用有參數的和私有的構造函數,好比:this
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方法。spa
4). 使用Clone方法建立對象.net
不管什麼時候咱們調用一個對象的clone方法,JVM都會幫咱們建立一個新的、同樣的對象,特別須要說明的是,用clone方法建立對象的過程當中並不會調用任何構造函數。關於如何使用clone方法以及淺克隆/深克隆機制,筆者已經在博文《 Java String 綜述(下篇)》作了詳細的說明。簡單而言,要想使用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要求構造函數的第一條語句必須是超類構造函數的調用語句),構造函數自己的代碼以前。例如:
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初始化以前和構造函數調用以前。
二、構造函數初始化
咱們能夠從上文知道,實例變量初始化與實例代碼塊初始化老是發生在構造函數初始化以前,那麼咱們下面着重看看構造函數初始化過程。衆所周知,每個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類,再依次對如下各種進行實例化,直到完成對目標類的實例化。具體而言,在實例化每一個類時,都遵循以下順序:先依次執行實例變量初始化和實例代碼塊初始化,再執行構造函數初始化。也就是說,編譯器會將實例變量初始化和實例代碼塊初始化相關代碼放到類的構造函數中去,而且這些代碼會被放在對超類構造函數的調用語句以後,構造函數自己的代碼以前。
Ps: 關於遞歸的思想與內涵的介紹,請參見個人博文《 算法設計方法:遞歸的內涵與經典應用》。
四、實例變量初始化、實例代碼塊初始化以及構造函數初始化綜合實例
筆者在《 JVM類加載機制概述:加載時機與加載過程》一文中詳細闡述了類初始化時機和初始化過程,並在文章的最後留了一個懸念給各位,這裏來揭開這個懸念。建議讀者先看完《 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 *///:~
根據上文所述的類實例化過程,咱們能夠將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類構造函數的等價變換中直接看出。
關於類的初始化時機,筆者在博文《 JVM類加載機制概述:加載時機與加載過程》已經介紹的很清楚了,此處再也不贅述。簡單地說,在類加載過程當中,準備階段是正式爲類變量(static 成員變量)分配內存並設置類變量初始值(零值)的階段,而初始化階段是真正開始執行類中定義的java程序代碼(字節碼)並按程序猿的意圖去初始化類變量的過程。更直接地說,初始化階段就是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊static{}中的語句合併產生的,其中編譯器收集的順序是由語句在源文件中出現的順序所決定。
類構造器<clinit>()與實例構造器<init>()不一樣,它不須要程序員進行顯式調用,虛擬機會保證在子類類構造器<clinit>()執行以前,父類的類構造<clinit>()執行完畢。因爲父類的構造器<clinit>()先執行,也就意味着父類中定義的靜態代碼塊/靜態變量的初始化要優先於子類的靜態代碼塊/靜態變量的初始化執行。特別地,類構造器<clinit>()對於類或者接口來講並非必需的,若是一個類中沒有靜態代碼塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生產類構造器<clinit>()。此外,在同一個類加載器下,一個類只會被初始化一次,可是一個類能夠任意地實例化對象。也就是說,在一個類的生命週期中,類構造器<clinit>()最多會被虛擬機調用一次,而實例構造器<init>()則會被虛擬機調用屢次,只要程序員還在建立對象。
注意,這裏所謂的實例構造器<init>()是指收集類中的全部實例變量的賦值動做、實例代碼塊和構造函數合併產生的,相似於上文對Foo類的構造函數和Bar類的構造函數作的等價變換。
一、一個實例變量在對象初始化的過程當中會被賦值幾回?
咱們知道,JVM在爲一個對象分配完內存以後,會給每個實例變量賦予默認值,這個時候實例變量被第一次賦值,這個賦值過程是沒有辦法避免的。若是咱們在聲明實例變量x的同時對其進行了賦值操做,那麼這個時候,這個實例變量就被第二次賦值了。若是咱們在實例代碼塊中,又對變量x作了初始化操做,那麼這個時候,這個實例變量就被第三次賦值了。若是咱們在構造函數中,也對變量x作了初始化操做,那麼這個時候,變量x就被第四次賦值。也就是說,在Java的對象初始化過程當中,一個實例變量最多能夠被初始化4次。
二、類的初始化過程與類的實例化過程的異同?
類的初始化是指類加載過程當中的初始化階段對類變量按照程序猿的意圖進行賦值的過程;而類的實例化是指在類徹底加載到內存中後建立對象的過程。
三、假如一個類還未加載到內存中,那麼在建立一個該類的實例時,具體過程是怎樣的?
咱們知道,要想建立一個類的實例,必須先將該類加載到內存並進行初始化,也就是說,類初始化操做是在類實例化操做以前進行的,但並不意味着:只有類初始化操做結束後才能進行類實例化操做。例如,筆者在博文《 JVM類加載機制概述:加載時機與加載過程》中所提到的下面這個經典案例:
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>() -> 父類的成員變量和實例代碼塊 -> 父類的構造函數 -> 子類的成員變量和實例代碼塊 -> 子類的構造函數。