本文主要記錄JAVA中對象的初始化過程,包括實例變量的初始化和類變量的初始化以及final關鍵字對初始化的影響。另外,還討論了由于繼承原因,探討了引用變量的編譯時類型和運行時類型
一,實例變量的初始化
這里首先介紹下創建對象的過程:
類型為Dog的一個對象首次創建時,或者Dog類的static字段或static方法首次訪問時,Java解釋器必須找到Dog.class(在事先設定好的路徑里面搜索);
找到Dog.class后(它會創建一個Class對象),它的所有static初始化模塊都會運行。因此,static初始化僅發生一次——在Class對象首次載入的時候;
創建一個newDog()時,Dog對象的構建進程首先會在內存堆(Heap)里為一個Dog對象分配足夠多的存儲空間;
這種存儲空間會清為零,將Dog中的所有基本類型(Primitive)設為它們的默認值(0用于數字,以及boolean和char的等價設定);
進行成員字段定義時發生的所有初始化都會執行;
執行構造函數。
然后,開始對實例變量進行初始化。一共有三種方式對實例變量進行初始化:
①定義實例變量時指定初始值
②非靜態初始化塊中對實例變量進行初始化
③構造器中對實例變量進行初始化
當new對象初始化時,①②要先于③執行。而①②的順序則按照它們在源代碼中定義的順序來執行。
當實例變量使用了final關鍵字修飾時,如果是在定義該final實例變量時直接指定初始值進行的初始化(第①種方式),則:該變量的初始值在編譯時就被確定下來,那么該final變量就類似于“宏變量”,相當于JAVA中的直接常量。
1
2
3
4
5
6
7
8
9
10
|
public class Test { public static void main(String[] args) { final String str1 = "HelloWorld" ; final String str2 = "Hello" + "World" ; System.out.println(str1 == str2); //true final String str3 = "Hello" + String.valueOf( "World" ); System.out.println(str1 == str3); //false } } |
第8行輸出false,是因為:第7行中str3需要通過valueOf方法調用之后才能確定。而不是在編譯時確定。
再來看一個示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class Test { final String str1 = "HelloWorld" ; final String str2 = "Hello" + "World" ; final String str3; final String str4; { str3 = "HelloWorld" ; } { System.out.println(str1 == str2); //true System.out.println(str1 == str3); //true // System.out.println(str1 == str4);//compile error } public Test() { str4 = "HelloWorld" ; System.out.println(str1 == str4); //true } public static void main(String[] args) { new Test(); } } |
把第13行的注釋去掉,會報編譯錯誤“Theblankfinalfieldstr4maynothavebeeninitialized”
因為變量str4是在構造器中進行初始化的。而前面提到:①定義實例變量時直接指定初始值(str1和str2的初始化)、②非靜態初始化塊中對實例變量進行初始化(str3的初始化)要先于③構造器中對實例變量進行初始化。
另外,對于final修飾的實例變量必須顯示地對它進行初始化,而不是通過構造器(<clinit>)對之進行默認初始化。
1
2
3
4
|
public class Test { final String str1; //compile error---沒有顯示的使用①②③中的方式進行初始化 String str2; } |
str2可以通過構造器對之進行默認的初始化,初始化為null。而對于final修飾的變量 str1,必須顯示地使用 上面提到的三種方式進行初始化。如下面的這個Test.java(一共有22行的這個Test類)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class Test { final String str1 = "Hello" ; //定義實例變量時指定初始值 final String str2; //非靜態初始化塊中對實例變量進行初始化 final String str3; //構造器中對實例變量進行初始化 { str2 = "Hello" ; } public Test() { str3 = "Hello" ; } public void show(){ System.out.println(str1 + str1 == "HelloHello" ); //true System.out.println(str2 + str2 == "HelloHello" ); //false System.out.println(str3 + str3 == "HelloHello" ); //false } public static void main(String[] args) { new Test().show(); } } |
由于str1采用的是第①種方式進行的初始化,故在執行15行:str1+str1連接操作時,str1其實相當于“宏變量”
而str2和str3并不是“宏變量”,故16-17行輸出false
在非靜態初始化代碼塊中初始化變量和在構造器中初始化變量的一點小區別:因為構造器是可以重寫的,比如你把某個實例變量放在無參的構造器中進行初始化,但是在new對象時卻調用的是有參數的構造器,那就得注意該實例變量有沒有正確得到初始化了。
而放在非靜態初始化代碼塊中初始化變量時,不管是調用有參的構造器還是無參的構造器,非靜態初始化代碼塊都會執行。
二,類變量的初始化
類變量一共有兩個地方對之進行初始化:
?定義類變量時指定初始值
?靜態初始化代碼塊中進行初始化
不管new多少個對象,類變量的初始化只執行一次。
三,繼承對初始化的影響
主要是理解編譯時類型和運行時類型的不同,從這個不同中可以看出this關鍵字和super關鍵字的一些本質區別。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
class Fruit{ String color = "unknow" ; public Fruit getThis(){ return this ; } public void info(){ System.out.println( "fruit's method" ); } } public class Apple extends Fruit{ String color = "red" ; //與父類同名的實例變量 @Override public void info() { System.out.println( "apple's method" ); } public void accessFruitInfo(){ super .info(); } public Fruit getSuper(){ return super .getThis(); } //for test purpose public static void main(String[] args) { Apple a = new Apple(); Fruit f = a.getSuper(); //Fruit f2 = a.getThis(); //System.out.println(f == f2);//true System.out.println(a == f); //true System.out.println(a.color); //red System.out.println(f.color); //unknow a.info(); //"apple's method" f.info(); //"apple's method" a.accessFruitInfo(); //"fruit's method" } } |
值得注意的地方有以下幾個:
⒈第35行引用變量a和f都指向內存中的同一個對象,36-37行調用它們的屬性時,a.color是red,而f.color是unknow
因為,f變量的聲明類型(編譯時類型)為Fruit,當訪問屬性時是由聲明該變量的類型來決定的。
⒉第39-40行,a.info()和f.info()都輸出“apple'smethod”
因為,f變量的運行時類型為Apple,info()是Apple重載的父類的一個方法。調用方法時由變量的運行時類型來決定。
⒊關于this關鍵字
當在29行new一個Apple對象,在30行調用getSuper()方法時,最終是執行到第4行的returnthis
this的解釋是:返回調用本方法的對象。它返回的類型是Fruit類型(見getThis方法的返回值類型),但實際上是Apple對象導致的getThis方法的調用。故,這里的this的聲明類型是Fruit,而運行時類型是Apple
⒋關于super關鍵字
super與this是有區別的。this可以用來代表“當前對象”,可用return返回。而對于super而言,沒有returnsuper;這樣的語句。
super主要是為了:在子類中訪問父類中的屬性或者在子類中調用父類中的方法而引入的一個關鍵字。比如第24行。
⒌在父類的構造器中不要去調用被子類覆蓋的方法(Override),或者說在構造父類對象時,不要依賴于子類覆蓋了父類的那些方法。這樣很可能會導致初始化的失敗(沒有正確地初始化對象)
因為:前面第1點和第2點談到了,對象(變量)有聲明時類型(編譯時類型)和運行時類型。而方法的調用取決于運行時類型。
當new子類對象時,會首先去初始化父類的屬性,而此時對象的運行時類型是子類,因此父類的屬性的賦值若依賴于子類中重載的方法,會導致父類屬性得不到正確的初始化值。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
class Fruit{ String color; public Fruit() { color = this .getColor(); //父類color屬性初始化依賴于重載的方法getColor // color = getColor(); } public String getColor(){ return "unkonw" ; } @Override public String toString() { return color; } } public class Apple extends Fruit{ @Override public String getColor() { return "color: " + color; } // public Apple() { // color = "red"; // } public static void main(String[] args) { System.out.println( new Apple()); //color: null } } |
Fruit類的color屬性 沒有正確地被初始化為"unknow",而是為 null
主要是因為第5行 this.getColor()調用的是Apple類的getColor方法,而此時Apple類的color屬性是直接從Fruit類繼承的。
四,參考資料
瘋狂Java 突破程序員基本功的16課 第二章
Effective Java中文版 第2版 中文 PDF版 第二版第17條
原文鏈接:https://www.cnblogs.com/hapjin/p/5931220.html