# 4.4 成員初始化
Java盡自己的全力保證所有變量都能在使用前得到正確的初始化。若被定義成相對于一個方法的“局部”變量,這一保證就通過編譯期的出錯提示表現出來。因此,如果使用下述代碼:
```
void f() {
int i;
i++;
}
```
就會收到一條出錯提示消息,告訴你`i`可能尚未初始化。當然,編譯器也可為`i`賦予一個默認值,但它看起來更象一個程序員的失誤,此時默認值反而會“幫倒忙”。若強迫程序員提供一個初始值,就往往能夠幫他/她糾出程序里的“Bug”。
然而,若將基本類型設為一個類的數據成員,情況就會變得稍微有些不同。由于任何方法都可以初始化或使用那個數據,所以在正式使用數據前,若還是強迫程序員將其初始化成一個適當的值,就可能不是一種實際的做法。然而,若為其賦予一個垃圾值,同樣是非常不安全的。因此,一個類的所有基本類型數據成員都會保證獲得一個初始值。可用下面這段小程序看到這些值:
```
//: InitialValues.java
// Shows default initial values
class Measurement {
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.4.1 規定初始化
如果想自己為變量賦予一個初始值,又會發生什么情況呢?為達到這個目的,一個最直接的做法是在類內部定義變量的同時也為其賦值(注意在C++里不能這樣做,盡管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`)的一個錯誤(在第9章詳述)。
甚至可通過調用一個方法來提供初始值:
```
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.4.2 構造器初始化
可考慮用構造器執行初始化進程。這樣便可在編程時獲得更大的靈活程度,因為我們可以在運行期調用方法和采取行動,從而“現場”決定初始化值。但要注意這樣一件事情:不可妨礙自動初始化的進行,它在構造器進入之前就會發生。因此,假如使用下述代碼:
```
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章),必須采用這一語法格式。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物