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