深入理解Java對象的創建過程:類的初始化與實例化
參考:
深入理解Java對象的創建過程:類的初始化與實例化
類的初始化&實例化順序
一、Java對象創建方式
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);
}
}
經測試,上面的代碼,把Student的構造函數改為私有運行錯誤,報java.lang.NoSuchMethodException。需使用getDeclaredConstructor。
使用newInstance方法的這兩種方式創建對象使用的就是Java的反射機制,事實上Class的newInstance方法內部調用的也是Constructor的newInstance方法。
4). 使用Clone方法創建對象
無論何時我們調用一個對象的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);
}
}
二、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初始化之前和構造函數調用之前。
類的初始化&實例化順序
從大流程來說,類肯定是先初始化,再實例化的,這里得出第一個順序:
靜態域 --> 實例域 --> 構造函數。另外要符合任何子類的動作都會觸發父類:父類 --> 子類。所以得出原則:【先靜態后實例;先父類后子類】
而且同一個域的順序可以分成兩步: 創建-->賦值
對於靜態域,其先經過鏈接創建靜態變量,賦default值;再到初始化階段給靜態變量賦assign值和執行靜態代碼塊。同理於實例域,也是分成創建和賦值兩個部分,不同的只是加入構造函數(形參和代碼塊):先創建實例變量和構造函數形參以default值,然后對變量和形參賦assign值和執行實例代碼塊,最后執行構造函數的代碼。
前提先要執行父類,再到子類;另外同一層次的就按從上到下順序執行,如下面例子。
package com.jscai.java.classLoader;
public class LoaderLazy {
{
System.out.println("Parent Instance Code");
}
private PrintTmp p1 = new PrintTmp("Parent Instance Member");
static {
System.out.println("Parent Static Code");
}
private static PrintTmp p2 = new PrintTmp("Parent Static Member");
public LoaderLazy() {
System.out.println("Parent Constuctor");
}
}
class SubLoaderLazy extends LoaderLazy {
{
System.out.println("Sub Instance Code");
}
private PrintTmp p1 = new PrintTmp("Sub Instance Member");
static {
System.out.println("Sub Static Code");
}
private static PrintTmp p2 = new PrintTmp("Sub Static Member");
public SubLoaderLazy() {
System.out.println("Sub Constuctor");
}
}
class PrintTmp {
public PrintTmp(String strOut) {
System.out.println(strOut);
}
}
SubLoaderLazy test = new SubLoaderLazy();
輸出:
console:
Parent Static Code
Parent Static Member
Sub Static Code
Sub Static Member
Parent Instance Code
Parent Instance Member
Parent Constuctor
Sub Instance Code
Sub Instance Member
Sub Constuctor
另外我們看看下面的代碼,說明了靜態域先賦default值;然后按順序賦assign值,先執行tester = new Test(),這時候count1和count2還是0,所以自加后都是1。
private static Test tester = new Test();
private static int count1;
private static int count2 = 2;
public Test() {
count1++;
count2++;
System.out.println("c1=" +count1 + "; c2=" + count2);
}
輸出:
console:
c1=1; c2=1
順序:
父類靜態代碼塊(按static聲明的先后順序)--》子類靜態代碼塊(按static聲明的先后順序)--》
父類實例代碼塊(按聲明的先后順序)--》父類構造函數 --》
子類實例代碼塊(按聲明的先后順序)--》子類構造函數