章五 初始化和清理
在C語言中,大量的錯誤來自不正確的初始化;在C++中,大量的錯誤來自對new出來的對象沒有正確的delete。Java吸取以上兩者的教訓,在使用了C++良好的構造器設計的同時,將所有對象都定義在堆上,並通過阻止直接對對象進行操作,使用垃圾回收器,自動回收內存,保證了對變量的正確初始化和清理。需要注意的是,對於對象的數據成員,即類的非static域,Java進行自動初始化,保證了對象的所有成員都得到了初始化。對於不是對象的變量,如局部基本類型變量和引用變量,則實施對使用未初始化變量進行報錯和超出作用域後進行清理的初始化和清理錯誤。
本章的內容從對對象的初始化開始,引導讀者思考,自然而然的引入構造器的概念並引發對構造器起名的思考。再從名字複用的角度展開,討論方法重載。中間對對象的清理和初始化進行詳細講解,最後則介紹了數組和枚舉類型。
1 用構造器保證初始化
構造器設計初衷:保證每個對象都會被初始化
名稱設計:採用C++的方案,與類同名。從而避免與方法重名。
其他說明:
1. 構造器沒有返回值,不屬於方法(可以看做是靜態方法,但其本質上不屬於方法),不採用方法首字母小寫的編程風格(類名首字母大寫)
2. Java中,對象的初始化和創建綁定在一起(對象本身無名,沒法像C++一樣先聲明後創建)
默認構造器:同C++一樣,Java中沒有參數的構造器被稱爲默認構造器。若類中未定義構造函數,則編譯器自動創建默認構造器。但一旦定義任一構造器,則編譯器不會創建默認構造器。
2 方法重載
對於面向對象程序設計,可以把方法視爲對象的行爲。通過對行爲命名,可以使對象方便的進行各種動作(行爲),好的名字可以使得設計更易理解和修改。我們知道,一詞可以多義,同樣的方法名也可以對應不同的操作,這就是方法重載的由來。
區分方法重載:每個重載方法都有一個獨一無二的形參列表。包括:1.參數個數;2.參數類型;3.參數順序。根據上面三個標準可知,僅返回值類型不同無法構成方法重載。如a.f();
這行代碼所示,這種寫法在程序設計裏是很常見的,若存在兩個f()
,僅返回值不同,則上述代碼無法通過編譯。所以不能僅以返回值類型區分方法重載。
當方法重載涉及到基本數據類型時,會有類型轉換的問題。由第三章的內容可知,分爲兩種:窄化轉換和擴展轉換。對於窄化轉換,必須進行顯示類型轉換,不易出錯,如下述代碼所示:
static void f(int l) {
//
}
long arg = 55;
f((int)arg); // explicit type conversion
對於擴展轉換,由於系統會自動提升類型,因此會發生一些意想不到的錯誤。下面給出將基本類型傳遞給重載參數的幾個注意點:
- 對於char類型,需要提升時,會直接提升至int型
- 對於字面值常量,整數默認爲int型,小數默認爲float型
- 對於byte,short,int,float,逐級提升,double能表示的範圍最大。
3. this關鍵字
對於OOP(面向對象程序設計),用OO的語法表示發送消息給對象時,比較自然的語法應該是object.doSomething()
。但是這種表示方法無法反映當前狀態object的狀態。這裏,定義了this關鍵字,用以表示對調用方法的當前對象的引用。需要說明的是,this只能在方法內部使用。this的幾個常用場合:
- 通過this返回對當前對象的引用;
return this
- 通過this將當前對象的引用傳遞給別的方法;
transmit(this)
- 使用this在構造器內調用同類的其他構造器,語法:
this(a,b)
,該方法的注意事項:
- 1. 調用方和被調用方必須都爲構造器
- 2. 構造器調用語句必須位於第一句
- 3. 構造器調用語句只能有一句
static和this:
static表示與具體對象無關,與this明顯不能共存。通過於this對比,更夠更好的理解static。在static方法內無法直接調用非static方法,因爲static方法沒有隱含的this。
4. 清理:finalize()和垃圾回收器(GC)
在C++中,清理工作和內存回收都由析構函數來完成。在Java中,因爲沒有局部變量的概念,內存由垃圾收集器自動回收。因爲GC並非實時回收內存,所以即使定義了清理工作,也沒法保證其在指定位置被執行,因此Java取消了析構函數。對於確實需要執行清理工作的對象,Java工程師只能自行定義方法實現類似析構函數的操作。對於finalize()和GC,下面三點必須記住:
- 1. 對象可能不被垃圾回收(並非實時處理,垃圾收集後統一處理)
- 2. 垃圾回收不等於析構
- 3. 垃圾回收只與內存有關
finalize()方法
finalize()方法在Object類中即有定義如下:
protected void finalize() throws Throwable { }
由於內存回收由GC負責,所以finalize()方法僅限處理以創建對象以外的方式爲對象分配內存的情況,主要發生在使用本地方法的情形。
本地方法:在Java中調用非Java代碼的方式,一般爲C/C++
finalize()的另一種用法,用於驗證對象終結條件。使用System.gc()
可強制執行沒有對象引用的對象的finalize()方法。
GC工作原理
垃圾回收機制主要有兩種算法:引用計數法和可達分析算法。其中引用計數法存在循環引用的問題,在Java中,主要應用可達分析算法,思想如下:對活的對象,一定能最終追溯到其存活在堆棧或靜態存儲期之中的引用。對於發現的每個引用,追蹤其所引用的對象,再追蹤此對象包含的所有引用,以此反覆,直至訪問空間內形成的所有引用網絡。
在上述思路下,Java採用一種自適應的垃圾回收技術。交替使用停止-複製和標記-清掃模式。
停止-複製:先暫停程序的運行(不屬於後臺回收模式),然後將所有存活的對象從當前堆複製到另一個堆,沒有被複制的都是垃圾。被複制到新堆後,對象在新堆中緊湊排列,避免頁面錯誤。
缺點:會降低效率。一是對象的所有引用需要被修正,需要維護比實際多一倍的空間。二是在程序穩定後,垃圾較少,反覆複製浪費極大。標記-清掃:採用可達算法分析,對每個找到的存活對象,進行標記,此過程不回收任何對象。當全部標記完成,開始清理,釋放所有的未標記對象,並不進行復制動作。
缺點:速度較慢,剩下的堆空間不連續。
下面將說明Java的自適應垃圾回收機制:
Java虛擬機進行監視,若所有對象都很穩定,垃圾回收器效率較低,則切換到標記-清掃模式;同樣,JVM跟蹤標記-清掃的效果,要是堆空間出現很多碎片,便切回停止-複製方式;可以稱爲自適應的、分代的、停止-複製、標記-清掃式垃圾回收器。
5. 成員初始化
本節主要講自動初始化和指定初始化。
Java盡力保證所有變量在使用前都能被初始化。對於局部變量(局部基本類型變量和對象引用),若未經初始化即使用,則編譯器報錯。對於類的數據成員則實行自動初始化,具體結果如下:
Data Type | boolean | char | byte | short | int | long | float | double | reference |
---|---|---|---|---|---|---|---|---|---|
Initial Value | false | [ ] | 0 | 0 | 0 | 0 | 0.0 | 0.0 | null |
若不想使用Java對域的自動初始化值,可以在定義域時對其進行指定初始化。對於非static域,類的對象的每個實例的該字段的值相同,地址不同。
6.構造器初始化
本節主要講構造器初始化。需要注意的是,構造器初始化在自動初始化和指定初始化之後進行。其中,自動初始化必然被執行,類的域必然存在初值。
初始化順序:字段定義的順序
靜態變量初始化:static關鍵字不能應用於局部變量,只能作用域域。靜態域在類對象被第一次創建或其被第一次訪問時初始化,且只初始化一次。
static塊初始化:優先級同靜態域,與靜態域按定義的順序初始化,初始化條件也一樣。主要用於靜態域初始化。
非static實例初始化:用以初始化非靜態變量,等價於指定初始化。
下面以Dog類爲例,總結對象創建過程:
- 1. 當首次創建Dog類對象,或首次訪問Dog類的靜態方法/域時,Java解釋器查找類路徑以定位Dog.class文件。若存在繼承,則有必要時順着繼承樹一一定位。
- 2. 載入Dog.class,按先父類後子類,同類中按定義順序,對靜態域和static塊進行初始化。只執行一次。
- 3. 當用new Dog()創建對象時,在堆上爲該對象分配足夠的存儲空間,並將這片空間清零——基礎類型爲0,引用類型爲null。
- 4. 對父類進行初始化
- 5. 執行指定初始化和非static實例初始化
- 6. 執行構造器。
7. 數組初始化
數組:相同類型,用一個標識符封裝到一起的一個對象序列或基本類型序列。定義方式:
1. int[] arr
, 2.int arr[];
按照規範,採用定義1的方式。編譯器不允許指定數組的大小。以上僅定義了對數組的引用,必須初始化以創建相應的存儲空間。通過在沒有數組時定義數組引用,方便數組之間的賦值——傳引用。數組有一個固定成員——length,存儲數組元素個數,可以用以監督下標越界。
數組有三種初始化方式:
//Approach 1: initialize one by one
Integer[] a = new Integer[rand.nextInt(20)];
for(int i = 0; i < a.length; i++)
a[i] = rand.nextInt(500);
//Approach 2: initialize all at one
Integer[] a = { new Integer(1), new Integer(2),3, };
//Approach 3: can be used to return a reference to an array
Integer[] b = new Integer[]{new Integer(1), new Integer(2),3, };
可變參數列表
定義方式: printArray(Object ... args)
可變參數列表本質上是數組,可以用以存儲基本數據類型,參數個數可以爲0個。
8.枚舉類型
定義方式:採用enum關鍵字,如下:
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
} ///:~
兩個方法:
ordinal()
:返回某個特定enum常量在enum中聲明順序;static values()
:返回按enum常量聲明順序組成的對應數組。
枚舉類型用於switch語句:
Spiciness degree = new Spiceness();
switch(degree) {
case NOT: System.out.println("not spicy at all.");
break;
case MILD:
case MEDIUM: System.out.println("a little hot.");
break;
case HOT:
case FLAMING:
default: System.out.println("maybe too hot.");
}