第4 章:初始化和清除
本章開始介紹構建器,它的作用是擔保初始化的正確實現。對構建器的定義要涉及函式過載的概念(因為可能同時有幾個構建器)。隨後要討論的是清除過程,它並非肯定如想象的那麼簡單。用完一個物件後,通常 可以不必管它,垃圾收集器會自動介入,釋放由它佔據的記憶體。這裡詳細探討了垃圾收集器以及它的一些特點。在這一章的最後,我們將更貼近地觀察初始化過程:自動成員初始化、指定成員初始化、初始化的順 序、static(靜態)初始化以及陣列初始化等等。
1. 用構建器自動初始化
一旦建立一個物件:new XXX();就會分配相應的儲存空間,並呼叫構建器。這樣可保證在我們經手之前,物件得到正確的初始化。請注意所有方法首字母小寫的編碼規則並不適用於構建器。這是由於構建器的名字必須與類名完全相同
在Java 中,定義和初始化屬於統一的概念——兩者缺一不可。
構建器屬於一種較特殊的方法型別,因為它沒有返回值。這與void 返回值存在著明顯的區別。對於void 返回值,儘管方法本身不會自動返回什麼,但仍然可以讓它返回另一些東西。構建器則不同,它不僅什麼也不會自動返回,而且根本不能有任何選擇。若存在一個返回值,而且假設我們可以自行選擇返回內容,那麼編譯器多少要知道如何對那個返回值作什麼樣的處理。
2.方法過載
由於構建器的名字由類名決定,所以只能有一個構建器名稱。但假若我們想用多種方式建立一個物件呢?例如,假設我們想建立一個類,令其用標準方式進行初始化,另外從檔案裡讀取資訊來初始化。此時,我們需要兩個構建器,一個沒有自變數(預設構建器),另一個將字串作為自變數——用於初始化物件的那個檔案的名字。由於都是構建器,所以它們必須有2.1 區分過載方法
若方法有同樣的名字,Java 怎樣知道我們指的哪一個方法呢?這裡有一個簡單的規則:每個過載的方法都必須採取獨一無二的自變數型別列表。除根據自變數的型別,程式設計師如何區分兩個同名方法的差異呢?
即使自變數的順序也足夠我們區分兩個方法(儘管我們通常不願意採用這種方法,因為它會產生難以維護的程式碼):
//: OverloadingOrder.java // Overloading based on the order of // the arguments. public class OverloadingOrder { static void print(String s, int i) { System.out.println( "String: " + s + ", int: " + i); } static void print(int i, String s) { System.out.println( "int: " + i + ", String: " + s); } public static void main(String[] args) { print("String first", 11); print(99, "Int first"); } }
兩個print()方法有完全一致的自變數,但順序不同,可據此區分它們。
2.2 返回值過載
為什麼只有類名和方法自變數列出?為什麼不根據返回值對方法加以區分?比如對下面這兩個方法來說,雖然它們有同樣的名字和自變數,但其實是很容易區分的:void f() {}
int f() {}
若編譯器可根據上下文(語境)明確判斷出含義,比如在int x=f()中,那麼這樣做完全沒有問題。然而,我們也可能呼叫一個方法,同時忽略返回值;我們通常把這稱為“為它的副作用去呼叫一個方法”,因為我們關心的不是返回值,而是方法呼叫的其他效果。所以假如我們象下面這樣呼叫方法:
f();
Java 怎樣判斷f()的具體呼叫方式呢?而且別人如何識別並理解程式碼呢?由於存在這一類的問題,所以不能根據返回值型別來區分過載的方法。
2.3 預設構建器
正如早先指出的那樣,預設構建器是沒有自變數的。它們的作用是建立一個“空物件”。若建立一個沒有構建器的類,則編譯程式會幫我們自動建立一個預設構建器。例如:
//: DefaultConstructor.java
class Bird {
int i;
}
public class DefaultConstructor {
public static void main(String[] args) {
Bird nc = new Bird(); // default!
}
}
對於下面這一行:new Bird();
它的作用是新建一個物件,並呼叫預設構建器——即使尚未明確定義一個象這樣的構建器。若沒有它,就沒有方法可以呼叫,無法構建我們的物件。然而,如果已經定義了一個構建器(無論是否有自變數),編譯程式都不會幫我們自動合成一個:
class Bush {
Bush(int i) {}
Bush(double d) {}
}
現在,假若使用下述程式碼:
new Bush();
編譯程式就會報告自己找不到一個相符的構建器。就好象我們沒有設定任何構建器,編譯程式會說:“你看來似乎需要一個構建器,所以讓我們給你製造一個吧。”但假如我們寫了一個構建器,編譯程式就會說:“啊,你已寫了一個構建器,所以我知道你想幹什麼;如果你不放置一個預設的,是由於你打算省略它。”
2.4 this 關鍵字
this 關鍵字(注意只能在方法內部使用)可為已呼叫了其方法的那個物件生成相應的控制代碼。可象對待其他任何物件控制代碼一樣對待這個控制代碼。但要注意,假若準備從自己某個類的另一個方法內部呼叫一個類方法,就不必使用this。只需簡單地呼叫那個方法即可。當前的this 控制代碼會自動應用於其他方法。所以我們能使用下面這樣的程式碼:
class Apricot {
void pick() { /* ... */ }
void pit() { pick(); /* ... */ } //從自己某個類的另一個方法內部呼叫一個類方法,就不必使用this
} //在pit()內部,我們可以說this.pick(),但事實上無此必要。編譯器能幫我們自動完成。
this 關鍵字只能用於那些特殊的類——需明確使用當前物件的控制代碼。例如,假若您希望將控制代碼返回給當前物件,那麼它經常在return 語句中使用。
//: Leaf.java
// Simple use of the "this" keyword
public class Leaf {
private int i = 0;
Leaf increment() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print(); //對同一個物件執行多項操作。
}
}
由於increment()通過this 關鍵字返回當前物件的控制代碼,所以可以方便地對同一個物件執行多項操作。public class Flower {
private int petalCount = 0;
private String s = new String("null");
Flower(int petals) {
petalCount = petals;
System.out.println("Constructor w/ int arg only, petalCount= "
+ petalCount);
}
Flower(String ss) {
System.out.println("Constructor w/ String arg only, s=" + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//! this(s); // Can't call two!
this.s = s; // Another use of "this"
System.out.println("String & int args");
}
Flower() {
this("hi", 47);
System.out.println("default constructor (no args)");
}
void print() {
//! this(11); // Not inside non-constructor!
System.out.println("petalCount = " + petalCount + " s = "+ s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.print();
}
}
其中,構建器Flower(String s,int petals)向我們揭示出這樣一個問題:儘管可用this 呼叫一個構建器,但不可呼叫兩個。除此以外,構建器呼叫必須是我們做的第一件事情,否則會收到編譯程式的報錯資訊。這個例子也向大家展示了this 的另一項用途。由於自變數s 的名字以及成員資料s 的名字是相同的,所以會出現混淆。為解決這個問題,可用this.s 來引用成員資料。經常都會在Java 程式碼裡看到這種形式的應用,在print()中,我們發現編譯器不讓我們從除了一個構建器之外的其他任何方法內部呼叫一個構建器。
2.5 static 的含義
static(靜態)方法的含義。它意味著一個特定的方法沒有this。我們不可從一個static 方法內部發出對非static 方法的呼叫,儘管反過來說是可以的。而且在沒有任何物件的前提下,我們可針對類本身發出對一個static 方法的呼叫。事實上,那正是static方法最基本的意義。它就好象我們建立一個全域性函式的等價物(在C 語言中)。除了全域性函式不允許在Java中使用以外,若將一個static 方法置入一個類的內部,它就可以訪問其他static 方法以及static 欄位。
3. 清除:收尾和垃圾收集
程式設計師都知道“初始化”的重要性,但通常忘記清除的重要性。畢竟,誰需要來清除一個int 呢?但是對於庫來說,用完後簡單地“釋放”一個物件並非總是安全的。當然,Java 可用垃圾收集器回收由不再使用的物件佔據的記憶體。現在考慮一種非常特殊且不多見的情況。假定我們的物件分配了一個“特殊”記憶體區域,沒有使用new。垃圾收集器只知道釋放那些由new 分配的記憶體,所以不知道如何釋放物件的“特殊”記憶體。為解決這個問題,Java 提供了一個名為finalize()的方法,可為我們的類定義它。在理想情況下,它的工作原理應該是這樣的:一旦垃圾收集器準備好釋放物件佔用的儲存空間,它首先呼叫finalize(),而且只有在下一次垃圾收集過程中,才會真正回收物件的記憶體。所以如果使用finalize(),就可以在垃圾收集期間進行一些重要的清除或清掃工作。
但也是一個潛在的程式設計陷阱,因為有些程式設計師(特別是在C++開發背景的)剛開始可能會錯誤認為它就是在C++中為“破壞器”(Destructor)使用的finalize()——破壞(清除)一個物件的時候,肯定會呼叫這個函式。但在這裡有必要區分一下C++和Java 的區別,因為C++的物件肯定會被清除(排開程式設計錯誤的因素),而Java 物件並非肯定能作為垃圾被“收集”去。或者換句話說:垃圾收集並不等於“破壞”!
若能時刻牢記這一點,踩到陷阱的可能性就會大大減少。它意味著在我們不再需要一個物件之前,有些行動是必須採取的,而且必須由自己來採取這些行動。Java 並未提供“破壞器”或者類似的概念,所以必須建立一個原始的方法,用它來進行這種清除。例如,假設在物件建立過程中,它會將自己描繪到螢幕上。如果不從螢幕明確刪除它的影象,那麼它可能永遠都不會被清除。若在finalize()裡置入某種刪除機制,那麼假設物件被當作垃圾收掉了,影象首先會將自身從螢幕上移去。但若未被收掉,影象就會保留下來。所以要記住的第二個重點是:我們的物件可能不會當作垃圾被收掉!
有時可能發現一個物件的儲存空間永遠都不會釋放,因為自己的程式永遠都接近於用光空間的臨界點。若程式執行結束,而且垃圾收集器一直都沒有釋放我們建立的任何物件的儲存空間,則隨著程式的退出,那些資源會返回給作業系統。這是一件好事情,因為垃圾收集本身也要消耗一些開銷。如永遠都不用它,那麼永遠也不用支出這部分開銷。
3 . 1 finalize ( ) 用途何在
大家可能已相信了自己應該將finalize()作為一種常規用途的清除方法使用。它有什麼好處呢?
要記住的第三個重點是:
垃圾收集只跟記憶體有關!
垃圾收集器存在的唯一原因是為了回收程式不再使用的記憶體。所以對於與垃圾收集有關的任何活動來說,其中最值得注意的是finalize()方法,它們也必須同記憶體以及它的回收有關。但這是否意味著假如物件包含了其他物件,finalize()就應該明確釋放那些物件呢?答案是否定的——垃圾收集器會負責釋放所有物件佔據的記憶體,無論這些物件是如何建立的。它將對finalize()的需求限制到特殊的情況。在這種情況下,我們的物件可採用與建立物件時不同的方法分配一些儲存空間。但大家或許會注意到,Java 中的所有東西都是物件,所以這到底是怎麼一回事呢?之所以要使用finalize(),看起來似乎是由於有時需要採取與Java 的普通方法不同的一種方法,通過分記憶體來做一些具有C 風格的事情。這主要可以通過“固有方法”來進行,它是從Java 裡呼叫非Java 方法的
一種方式(固有方法的問題在附錄A 討論)。C 和C++是目前唯一獲得固有方法支援的語言。但由於它們能調用通過其他語言編寫的子程式,所以能夠有效地呼叫任何東西。在非Java 程式碼內部,也許能呼叫C 的malloc()系列函式,用它分配儲存空間。而且除非呼叫了free(),否則儲存空間不會得到釋放,從而造成內存“漏洞”的出現。當然,free()是一個C 和C++函式,所以我們需要在finalize()內部的一個固有方法中呼叫它。
讀完上述文字後,大家或許已弄清楚了自己不必過多地使用finalize()。這個思想是正確的;它並不是進行
普通清除工作的理想場所。那麼,普通的清除工作應在何處進行呢?
3.2 必須執行清除
為清除一個物件,那個物件的使用者必須在希望進行清除的地點呼叫一個清除方法。這聽起來似乎很容易做到,但卻與C++“破壞器”的概念稍有抵觸。在C++中,所有物件都會破壞(清除)。或者換句話說,所有對\\\\\\\\\\\象都“應該”破壞。若將C++物件建立成一個本地物件,比如在堆疊中建立(在Java 中是不可能的),那麼清除或破壞工作就會在“結束花括號”所代表的、建立這個物件的作用域的末尾進行。若物件是用new 建立的(類似於Java),那麼當程式設計師呼叫C++的delete 命令時(Java 沒有這個命令),就會呼叫相應的破壞器。若程式設計師忘記了,那麼永遠不會呼叫破壞器,我們最終得到的將是一個記憶體“漏洞”,另外還包括物件的其他部分永遠不會得到清除。
相反,Java 不允許我們建立本地(區域性)物件——無論如何都要使用new。但在Java 中,沒有“delete”命令來釋放物件,因為垃圾收集器會幫助我們自動釋放儲存空間。所以如果站在比較簡化的立場,我們可以說正是由於存在垃圾收集機制,所以Java 沒有破壞器。然而,隨著以後學習的深入,就會知道垃圾收集器的存在並不能完全消除對破壞器的需要,或者說不能消除對破壞器代表的那種機制的需要(而且絕對不能直接呼叫finalize(),所以應儘量避免用它)。若希望執行除釋放儲存空間之外的其他某種形式的清除工作,仍然必須呼叫Java 中的一個方法。它等價於C++的破壞器,只是沒後者方便。
//: Garbage.java
// Demonstration of the garbage
// collector and finalization
class Chair {
static boolean gcrun = false;
static boolean f = false;
static int created = 0;
static int finalized = 0;
int i;
Chair() {
i = ++created;
if(created == 47)
System.out.println("Created 47");
}
protected void finalize() {
if(!gcrun) {
gcrun = true;
System.out.println(
"Beginning to finalize after " +
created + " Chairs have been created");
}
if(i == 47) {
System.out.println(
"Finalizing Chair #47, " +
"Setting flag to stop Chair creation");
f = true;
}
finalized++;
if(finalized >= created)
System.out.println(
107
"All " + finalized + " finalized");
}
}
public class Garbage {
public static void main(String[] args) {
if(args.length == 0) {
System.err.println("Usage: \n" +
"java Garbage before\n or:\n" +
"java Garbage after");
return;
}
while(!Chair.f) {
new Chair();
new String("To take up space");
}
System.out.println(
"After all Chairs have been created:\n" +
"total created = " + Chair.created +
", total finalized = " + Chair.finalized);
if(args[0].equals("before")) {
System.out.println("gc():");
System.gc();
System.out.println("runFinalization():");
System.runFinalization();
}
System.out.println("bye!");
if(args[0].equals("after"))
System.runFinalizersOnExit(true);
}
}
上面這個程式建立了許多Chair 物件,而且在垃圾收集器開始執行後的某些時候,程式會停止建立Chair。由於垃圾收集器可能在任何時間執行,所以我們不能準確知道它在何時啟動。因此,程式用一個名為gcrun的標記來指出垃圾收集器是否已經開始執行。利用第二個標記f,Chair 可告訴main()它應停止物件的生成。這兩個標記都是在finalize()內部設定的,它調用於垃圾收集期間。另兩個static 變數——created 以及finalized——分別用於跟蹤已建立的物件數量以及垃圾收集器已進行完收尾工作的物件數量。最後,每個Chair 都有它自己的(非static)int i,所以能跟蹤瞭解它具體的編號是多少。編號為47 的Chair 進行完收尾工作後,標記會設為true ,最終結束Chair 物件的建立過程。
所有這些都在main()的內部進行——在下面這個迴圈裡:
while(!Chair.f) {
new Chair();
new String("To take up space");
}
會疑惑這個迴圈什麼時候會停下來,因為內部沒有任何改變Chair.f 值的語句。然而,finalize()程序會改變這個值,直至最終對編號47 的物件進行收尾處理。每次迴圈過程中建立的String 物件只是屬於額外的垃圾,用於吸引垃圾收集器——一旦垃圾收集器對可用記憶體的容量感到“緊張不安”,就會開始關注它。執行這個程式的時候,提供了一個命令列自變數“before”或者“after”。其中,“before”自變數會呼叫System.gc()方法(強制執行垃圾收集器),同時還會呼叫System.runFinalization()方法,以便進行收尾工作。這些方法都可在Java 1.0 中使用,但通過使用“after”自變數而呼叫的runFinalizersOnExit()方法卻只有Java 1.1 及後續版本提供了對它的支援。注意可在程式執行的任何時候呼叫這個方法,而且收尾程式的執行與垃圾收集器是否執行是無關的。
4 成員初始化
Java 儘自己的全力保證所有變數都能在使用前得到正確的初始化。若被定義成相對於一個方法的“區域性”變數,這一保證就通過編譯期的出錯提示表現出來。因此,如果使用下述程式碼:
void f() {
int i;
i++;
}
就會收到一條出錯提示訊息,告訴你i 可能尚未初始化。當然,編譯器也可為i 賦予一個預設值,但它看起來更象一個程式設計師的失誤,此時預設值反而會“幫倒忙”。若強迫程式設計師提供一個初始值,就往往能夠幫他/她糾出程式裡的“bug”。然而,若將基本型別(主型別)設為一個類的資料成員,情況就會變得稍微有些不同。由於任何方法都可以初始化或使用那個資料,所以在正式使用資料前,若還是強迫程式設計師將其初始化成一個適當的值,就可能不是一種實際的做法。然而,若為其賦予一個垃圾值,同樣是非常不安全的。因此,一個類的所有基本型別資料成員都會保證獲得一個初始值。可用下面這段小程式看到這些值://: InitialValues.java
// Shows default initial values
class Measurement {
109
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
void print() {
System.out.println(
"Data type Inital value\n" +
"boolean " + t + "\n" +
"char " + c + "\n" +
"byte " + b + "\n" +
"short " + s + "\n" +
"int " + i + "\n" +
"long " + l + "\n" +
"float " + f + "\n" +
"double " + d);
}
}
public class InitialValues {
public static void main(String[] args) {
Measurement d = new Measurement();
d.print();
/* In this case you could also say:
new Measurement().print();
*/
}
} ///:~
輸入結果如下:
Data type Inital value
boolean false
char
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
其中,Char 值為空(NULL),沒有資料打印出來。稍後大家就會看到:在一個類的內部定義一個物件控制代碼時,如果不將其初始化成新物件,那個控制代碼就會獲得一個空值。4 . 1 規定初始化
如果想自己為變數賦予一個初始值,又會發生什麼情況呢?為達到這個目的,一個最直接的做法是在類內部定義變數的同時也為其賦值(注意在C++裡不能這樣做)。在下面,Measurement 類內部的欄位定義已發生了變化,提供了初始值:
class Measurement {
boolean b = true;
char c = 'x';
byte B = 47;
short s = 0xff;
int i = 999;
long l = 1;
float f = 3.14f;
double d = 3.14159;
//. .
亦可用相同的方法初始化非基本(主)型別的物件。若Depth 是一個類,那麼可象下面這樣插入一個變數並進行初始化:class Measurement {
Depth o = new Depth();
boolean b = true;
// . . .
若尚未為o 指定一個初始值,同時不顧一切地提前試用它,就會得到一條執行期錯誤提示,告訴你產生了名為“違例”(Exception)的一個錯誤。甚至可通過呼叫一個方法來提供初始值:
class CInit {
int i = f();
//...
}
當然,這個方法亦可使用自變數,但那些自變數不可是尚未初始化的其他類成員。因此,下面這樣做是合法的:class CInit {
int i = f();
int j = g(i);
//...
}
但下面這樣做是非法的:
class CInit {
int j = g(i);
int i = f();
//...
}
這正是編譯器對“向前引用”感到不適應的一個地方,因為它與初始化的順序有關,而不是與程式的編譯方式有關。這種初始化方法非常簡單和直觀。它的一個限制是型別Measurement 的每個物件都會獲得相同的初始化值。有時,這正是我們希望的結果,但有時卻需要盼望更大的靈活性。
4 . 2 構建器初始化
可考慮用構建器執行初始化程序。這樣便可在程式設計時獲得更大的靈活程度,因為我們可以在執行期呼叫方法和採取行動,從而“現場”決定初始化值。但要注意這樣一件事情:不可妨礙自動初始化的進行,它在構建器進入之前就會發生。因此,假如使用下述程式碼:
class Counter {
int i;
Counter() { i = 7; }
// . . .
那麼i 首先會初始化成零,然後變成7。對於所有基本型別以及物件控制代碼,這種情況都是成立的,其中包括在定義時已進行了明確初始化的那些一些。考慮到這個原因,編譯器不會試著強迫我們在構建器任何特定的
場所對元素進行初始化,或者在它們使用之前——初始化早已得到了保證.
1. 初始化順序在一個類裡,初始化的順序是由變數在類內的定義順序決定的。即使變數定義大量遍佈於方法定義的中間,那些變數仍會在呼叫任何方法之前得到初始化——甚至在構建器呼叫之前。例如:
//: OrderOfInitialization.java
// Demonstrates initialization order.
// When the constructor is called, to create a
// Tag object, you'll see a message:
class Tag {
Tag(int marker) {
System.out.println("Tag(" + marker + ")");
}
}
class Card {
Tag t1 = new Tag(1); // Before constructor
Card() {
// Indicate we're in the constructor:
System.out.println("Card()");
t3 = new Tag(33); // Re-initialize t3
}
Tag t2 = new Tag(2); // After constructor
void f() {
System.out.println("f()");
}
Tag t3 = new Tag(3); // At end
}
public class OrderOfInitialization {
public static void main(String[] args) {
Card t = new Card();
t.f(); // Shows that construction is done
}
} ///:~
在Card 中,Tag 物件的定義故意到處散佈,以證明它們全都會在構建器進入或者發生其他任何事情之前得到初始化。除此之外,t3 在構建器內部得到了重新初始化。它的輸入結果如下:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
因此,t3 控制代碼會被初始化兩次,一次在構建器呼叫前,一次在呼叫期間(第一個物件會被丟棄,所以它後來可被當作垃圾收掉)。從表面看,這樣做似乎效率低下,但它能保證正確的初始化——若定義了一個過載的構建器,它沒有初始化t3;同時在t3 的定義裡並沒有規定“預設”的初始化方式,那麼會產生什麼後果呢?
2. 靜態資料的初始化
若資料是靜態的(static),那麼同樣的事情就會發生;如果它屬於一個基本型別(主型別),而且未對其初始化,就會自動獲得自己的標準基本型別初始值;如果它是指向一個物件的控制代碼,那麼除非新建一個物件,並將控制代碼同它連線起來,否則就會得到一個空值(NULL)。如果想在定義的同時進行初始化,採取的方法與非靜態值表面看起來是相同的。但由於static 值只有一個存
儲區域,所以無論建立多少個物件,都必然會遇到何時對那個儲存區域進行初始化的問題。下面這個例子可將這個問題說更清楚一些:
//: StaticInitialization.java
// Specifying initial values in a
// class definition.
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Table {
static Bowl b1 = new Bowl(1);
Table() {
System.out.println("Table()");
b2.f(1);
}
void f2(int marker) {
System.out.println("f2(" + marker + ")");
}
static Bowl b2 = new Bowl(2);
}
class Cupboard {
Bowl b3 = new Bowl(3);
static Bowl b4 = new Bowl(4);
Cupboard() {
System.out.println("Cupboard()");
b4.f(2);
}
void f3(int marker) {
System.out.println("f3(" + marker + ")");
}
static Bowl b5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
System.out.println(
"Creating new Cupboard() in main");
new Cupboard();
System.out.println(
"Creating new Cupboard() in main");
new Cupboard();
t2.f2(1);
t3.f3(1);
}
static Table t2 = new Table();
static Cupboard t3 = new Cupboard();
} ///:~
Bowl 允許我們檢查一個類的建立過程,而Table 和Cupboard 能建立散佈於類定義中的Bowl 的static 成員。注意在static 定義之前,Cupboard 先建立了一個非static 的Bowl b3。它的輸出結果如下:Bowl(1)
Bowl(2)
Table()
f(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
f2(1)
f3(1)
static 初始化只有在必要的時候才會進行。如果不建立一個Table 物件,而且永遠都不引用Table.b1 或Table.b2,那麼static Bowl b1 和b2 永遠都不會建立。然而,只有在建立了第一個Table 物件之後(或者發生了第一次static 訪問),它們才會建立。在那以後,static 物件不會重新初始化。初始化的順序是首先static(如果它們尚未由前一次物件建立過程初始化),接著是非static 物件。大家可從輸出結果中找到相應的證據。在這裡有必要總結一下物件的建立過程。請考慮一個名為Dog 的類:
(1) 型別為Dog 的一個物件首次建立時,或者Dog 類的static 方法/static 欄位首次訪問時,Java 直譯器必須找到Dog.class(在事先設好的類路徑裡搜尋)。
(2) 找到Dog.class 後(它會建立一個Class 物件,這將在後面學到),它的所有static 初始化模組都會執行。因此,static 初始化僅發生一次——在Class 物件首次載入的時候。
(3) 建立一個new Dog()時,Dog 物件的構建程序首先會在記憶體堆(Heap)裡為一個Dog 物件分配足夠多的儲存空間。
(4) 這種儲存空間會清為零,將Dog 中的所有基本型別設為它們的預設值(零用於數字,以及boolean 和char 的等價設定)。
(5) 進行欄位定義時發生的所有初始化都會執行。
(6) 執行構建器。這實際可能要求進行相當多的操作,特別是在涉及繼承的時候。
3. 明確進行的靜態初始化
Java 允許我們將其他static 初始化工作劃分到類內一個特殊的“static 構建從句”(有時也叫作“靜態塊”)裡。它看起來象下面這個樣子:
class Spoon {
static int i;
static {
i = 47;
}
// . . .
儘管看起來象個方法,但它實際只是一個static 關鍵字,後面跟隨一個方法主體。與其他static 初始化一樣,這段程式碼僅執行一次——首次生成那個類的一個物件時,或者首次訪問屬於那個類的一個static 成員時(即便從未生成過那個類的物件)。例如:
//: ExplicitStatic.java
// Explicit static initialization
// with the "static" clause.
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup c1;
static Cup c2;
static {
c1 = new Cup(1);
c2 = new Cup(2);
}
Cups() {
System.out.println("Cups()");
115
}
}
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.c1.f(99); // (1)
}
static Cups x = new Cups(); // (2)
static Cups y = new Cups(); // (2)
} ///:~
在標記為(1)的行內訪問static 物件c1 的時候,或在行(1)標記為註釋,同時(2)行不標記成註釋的時候,用於Cups 的static 初始化模組就會執行。若(1)和(2)都被標記成註釋,則用於Cups 的static 初始化程序永遠不會發生。4. 非靜態例項的初始化
針對每個物件的非靜態變數的初始化,Java 提供了一種類似的語法格式。下面是一個例子:
//: Mugs.java
// Java 1.1 "Instance Initialization"
class Mug {
Mug(int marker) {
System.out.println("Mug(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
public class Mugs {
Mug c1;
Mug c2;
{
c1 = new Mug(1);
c2 = new Mug(2); //匿名內部類
System.out.println("c1 & c2 initialized"); }
Mugs() {
System.out.println("Mugs()");
}
public static void main(String[] args) {
System.out.println("Inside main()");
Mugs x = new Mugs();
}
} ///:~
大家可看到例項初始化從句:{
c1 = new Mug(1);
c2 = new Mug(2);
System.out.println("c1 & c2 initialized");
}
它看起來與靜態初始化從句極其相似,只是static 關鍵字從裡面消失了。為支援對“匿名內部類”的初始化(參見第7 章),必須採用這一語法格式。
5 陣列初始化
陣列代表一系列物件或者基本資料型別,所有相同的型別都封裝到一起——採用一個統一的識別符號名稱。陣列的定義和使用是通過方括號索引運算子進行的([])。為定義一個數組,只需在型別名後簡單地跟隨一對空方括號即可:int[] al;
也可以將方括號置於識別符號後面,獲得完全一致的結果:
int al[];
編譯器不允許我們告訴它一個數組有多大。這樣便使我們回到了“控制代碼”的問題上。此時,我們擁有的一切就是指向陣列的一個控制代碼,而且尚未給陣列分配任何空間。為了給陣列建立相應的儲存空間,必須編寫一個初始化表示式。對於陣列,初始化工作可在程式碼的任何地方出現,但也可以使用一種特殊的初始化表示式,它必須在陣列建立的地方出現。這種特殊的初始化是一系列由花括號封閉起來的值。儲存空間的分配(等價於使用new)將由編譯器在這種情況下進行。例如:
int[] a1 = { 1, 2, 3, 4, 5 };
那麼為什麼還要定義一個沒有陣列的陣列控制代碼呢?
int[] a2;
事實上在Java 中,可將一個數組分配給另一個,所以能使用下述語句:
a2 = a1;
我們真正準備做的是複製一個控制代碼,就象下面演示的那樣:
//: Arrays.java
// Arrays of primitives.
public class Arrays {
public static void main(String[] args) {
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2;
a2 = a1;
for(int i = 0; i < a2.length; i++)
a2[i]++;
for(int i = 0; i < a1.length; i++)
prt("a1[" + i + "] = " + a1[i]);
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
大家看到a1 獲得了一個初始值,而a2 沒有;a2 將在以後賦值——這種情況下是賦給另一個數組。這裡也出現了一些新東西:所有陣列都有一個本質成員(無論它們是物件陣列還是基本型別陣列),可對其進行查詢——但不是改變,從而獲知陣列內包含了多少個元素。這個成員就是length。與C 和C++類似,由於Java 陣列從元素0 開始計數,所以能索引的最大元素編號是“length-1”。如超出邊界,C 和C++會“默默”地接受,並允許我們胡亂使用自己的記憶體,這正是許多程式錯誤的根源。然而,Java 可保留我們這受這一問題的損害,方法是一旦超過邊界,就生成一個執行期錯誤(即一個“違例”,這是第9 章的主題)。當然,由於需要檢查每個陣列的訪問,所以會消耗一定的時間和多餘的程式碼量,而且沒有辦法把它關閉。這意味著陣列訪問可能成為程式效率低下的重要原因——如果它們在關鍵的場合進行。但考慮到因特網訪問的安全,以及程式設計師的程式設計效率,Java 設計人員還是應該把它看作是值得的。程式編寫期間,如果不知道在自己的數組裡需要多少元素,那麼又該怎麼辦呢?此時,只需簡單地用new 在數組裡建立元素。在這裡,即使準備建立的是一個基本資料型別的陣列,new 也能正常地工作(new 不會建立非陣列的基本型別):
//: ArrayNew.java
// Creating arrays with new.
import java.util.*;
public class ArrayNew {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
int[] a;
a = new int[pRand(20)];
prt("length of a = " + a.length);
for(int i = 0; i < a.length; i++)
prt("a[" + i + "] = " + a[i]);
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
由於陣列的大小是隨機決定的(使用早先定義的pRand()方法),所以非常明顯,陣列的建立實際是在執行期間進行的。除此以外,從這個程式的輸出中,大家可看到基本資料型別的陣列元素會自動初始化成“空”值(對於數值,空值就是零;對於char,它是null ;而對於boolean,它卻是false)。當然,陣列可能已在相同的語句中定義和初始化了,如下所示:int[] a = new int[pRand(20)];
若操作的是一個非基本型別物件的陣列,那麼無論如何都要使用new。在這裡,我們會再一次遇到控制代碼問題,因為我們建立的是一個控制代碼陣列。請大家觀察封裝器型別Integer,它是一個類,而非基本資料型別:
//: ArrayClassObj.java
// Creating an array of non-primitive objects.
import java.util.*;
public class ArrayClassObj {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
Integer[] a = new Integer[pRand(20)];
prt("length of a = " + a.length);
for(int i = 0; i < a.length; i++) {
118
a[i] = new Integer(pRand(500));
prt("a[" + i + "] = " + a[i]);
}
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
在這兒,甚至在new 呼叫後才開始建立陣列:
Integer[] a = new Integer[pRand(20)];
它只是一個控制代碼陣列,而且除非通過建立一個新的Integer 物件,從而初始化了物件控制代碼,否則初始化程序不會結束:
a[i] = new Integer(pRand(500));
但若忘記建立物件,就會在執行期試圖讀取空陣列位置時獲得一個“違例”錯誤。下面讓我們看看列印語句中String 物件的構成情況。大家可看到指向Integer 物件的控制代碼會自動轉換,從而產生一個String,它代表著位於物件內部的值。
亦可用花括號封閉列表來初始化物件陣列。可採用兩種形式,第一種是Java 1.0 允許的唯一形式。第二種(等價)形式自Java 1.1 才開始提供支援:
//: ArrayInit.java
// Array initialization
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = {
new Integer(1),
new Integer(2),
new Integer(3),
};
// Java 1.1 only:
Integer[] b = new Integer[] {
new Integer(1),
new Integer(2),
new Integer(3),
};
}
} ///:~
這種做法大多數時候都很有用,但限制也是最大的,因為陣列的大小是在編譯期間決定的。初始化列表的最後一個逗號是可選的(這一特性使長列表的維護變得更加容易)。陣列初始化的第二種形式(Java 1.1 開始支援)提供了一種更簡便的語法,可建立和呼叫方法,獲得與C 的“變數引數列表”(C 通常把它簡稱為“變參表”)一致的效果。這些效果包括未知的引數(自變數)數量以及未知的型別(如果這樣選擇的話)。由於所有類最終都是從通用的根類Object 中繼承的,所以能建立一個方法,令其獲取一個Object 陣列,並象下面這樣呼叫它:
//: VarArgs.java
// Using the Java 1.1 array syntax to create
// variable argument lists
class A { int i; }
119
public class VarArgs {
static void f(Object[] x) {
for(int i = 0; i < x.length; i++)
System.out.println(x[i]);
}
public static void main(String[] args) {
f(new Object[] {
new Integer(47), new VarArgs(),
new Float(3.14), new Double(11.11) });
f(new Object[] {"one", "two", "three" });
f(new Object[] {new A(), new A(), new A()});
}
} ///:~
此時,我們對這些未知的物件並不能採取太多的操作,而且這個程式利用自動String 轉換對每個Object 做一些有用的事情。在第11 章(執行期型別標識或RTTI),大家還會學習如何調查這類物件的準確型別,使自己能對它們做一些有趣的事情。5 . 1 多維陣列
在Java 裡可以方便地建立多維陣列:
//: MultiDimArray.java
// Creating multidimensional arrays.
import java.util.*;
public class MultiDimArray {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
for(int i = 0; i < a1.length; i++)
for(int j = 0; j < a1[i].length; j++)
prt("a1[" + i + "][" + j +
"] = " + a1[i][j]);
// 3-D array with fixed length:
int[][][] a2 = new int[2][2][4];
for(int i = 0; i < a2.length; i++)
for(int j = 0; j < a2[i].length; j++)
for(int k = 0; k < a2[i][j].length;
k++)
prt("a2[" + i + "][" +
j + "][" + k +
"] = " + a2[i][j][k]);
// 3-D array with varied-length vectors:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(5)];
}
for(int i = 0; i < a3.length; i++)
for(int j = 0; j < a3[i].length; j++)
for(int k = 0; k < a3[i][j].length;
k++)
prt("a3[" + i + "][" +
j + "][" + k +
"] = " + a3[i][j][k]);
// Array of non-primitive objects:
Integer[][] a4 = {
{ new Integer(1), new Integer(2)},
{ new Integer(3), new Integer(4)},
{ new Integer(5), new Integer(6)},
};
for(int i = 0; i < a4.length; i++)
for(int j = 0; j < a4[i].length; j++)
prt("a4[" + i + "][" + j +
"] = " + a4[i][j]);
Integer[][] a5;
a5 = new Integer[3][];
for(int i = 0; i < a5.length; i++) {
a5[i] = new Integer[3];
for(int j = 0; j < a5[i].length; j++)
a5[i][j] = new Integer(i*j);
}
for(int i = 0; i < a5.length; i++)
for(int j = 0; j < a5[i].length; j++)
prt("a5[" + i + "][" + j +
"] = " + a5[i][j]);
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
用於列印的程式碼裡使用了length,所以它不必依賴固定的陣列大小。第一個例子展示了基本資料型別的一個多維陣列。我們可用花括號定出陣列內每個向量的邊界:
int[][] a1 = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
每個方括號對都將我們移至陣列的下一級。第二個例子展示了用new 分配的一個三維陣列。在這裡,整個陣列都是立即分配的:
int[][][] a2 = new int[2][2][4];
但第三個例子卻向大家揭示出構成矩陣的每個向量都可以有任意的長度:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(5)];
}
對於第一個new 建立的陣列,它的第一個元素的長度是隨機的,其他元素的長度則沒有定義。for 迴圈內的第二個new 則會填寫元素,但保持第三個索引的未定狀態——直到碰到第三個new。
根據輸出結果,大家可以看到:假若沒有明確指定初始化值,陣列值就會自動初始化成零。
可用類似的表式處理非基本型別物件的陣列。這從第四個例子可以看出,它向我們演示了用花括號收集多個
new 表示式的能力:
Integer[][] a4 = {
{ new Integer(1), new Integer(2)},
{ new Integer(3), new Integer(4)},
{ new Integer(5), new Integer(6)},
};
第五個例子展示瞭如何逐漸構建非基本型別的物件陣列:
Integer[][] a5;
a5 = new Integer[3][];
for(int i = 0; i < a5.length; i++) {
a5[i] = new Integer[3];
for(int j = 0; j < a5[i].length; j++)
a5[i][j] = new Integer(i*j);
}
i*j 只是在Integer 裡置了一個有趣的值。