可考慮用構建器執行初始化進程。這樣便可在編程時獲得更大的靈活程度,因為我們可以在運行期調用方法和采取行動,從而“現場”決定初始化值。但要注意這樣一件事情:不可妨礙自動初始化的進行,它在構建器進入之前就會發生。因此,假如使用下述代碼:
class Counter {
int i;
Counter() { i = 7; }
// . . .
那麼i首先會初始化成零,然後變成7。對於所有基本類型以及對象句柄,這種情況都是成立的,其中包括在定義時已進行了明確初始化的那些一些。考慮到這個原因,編譯器不會試著強迫我們在構建器任何特定的場所對元素進行初始化,或者在它們使用之前——初始化早已得到了保證(注釋⑤)。
⑤:相反,C++有自己的“構建器初始模塊列表”,能在進入構建器主體之前進行初始化,而且它對於對象來說是強制進行的。參見《Thinking in C++》。
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) 執行構建器。正如第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()"); } } 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 1.1提供了一種類似的語法格式。下面是一個例子:
//: 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章),必須采用這一語法格式。