程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 6.JAVA編程思想初始化和清除

6.JAVA編程思想初始化和清除

編輯:JAVA綜合教程

6.JAVA編程思想初始化和清除


隨著計算機的進步,‘不安全’的程序設計已成為造成編程代價高昂的罪魁禍首之一。

許多 C程序的錯誤都是由於程序員忘記初始化一個變量造成的。對於現成的庫,若用戶不知道如何初始化庫的一個組件,就往往會出現這一類的錯誤。清除是另一個特殊的問題,因為用完一個元素後,由於不再關心,所以很容易把它忘記。這樣一來,那個元素占用的資

源會一直保留下去,極易產生資源(主要是內存)用盡的後果。

C++為我們引入了“構建器”的概念。這是一種特殊的方法,在一個對象創建之後自動調用。Java 也沿用了這個概念,但新增了自己的“垃圾收集器”,能在資源不再需要的時候自動釋放它們。

1 用構建器自動初始化

對於方法的創建,可將其想象成為自己寫的每個類都調用一次initialize()。這個名字提醒我們在使用對象之前,應首先進行這樣的調用。但不幸的是,這也意味著用戶必須記住調用方法。在 Java 中,由於提供了名為“構建器”的一種特殊方法,所以類的設計者可擔保每個對象都會得到正確的初始化。若某個類有一個構建器,那麼在創建對象時,Java 會自動調用那個構建器——甚至在用戶毫不知覺的情況下。

接著的一個問題是如何命名這個方法。存在兩方面的問題。第一個是我們使用的任何名字都可能與打算為某個類成員使用的名字沖突。第二是由於編譯器的責任是調用構建器,所以它必須知道要調用是哪個方法。C++采取的方案看來是最簡單的,且更有邏輯性,所以也在Java 裡得到了應用:構建器的名字與類名相同。這樣一來,可保證象這樣的一個方法會在初始化期間自動調用。

示例:

class Rock {

Rock() { // This is the constructor

System.out.println("CreatingRock");

}

}

publicclass test {

publicstaticvoid main(String[] args) {

for(inti = 0; i < 10;i++)

new Rock();

}

} ///:~

一旦創建一個對象:

new Rock();

就會分配相應的存儲空間,並調用構建器。這樣可保證在我們經手之前,對象得到正確的初始化。請注意所有方法首字母小寫的編碼規則並不適用於構建器。這是由於構建器的名字必須與類名完全相同! 和其他任何方法一樣,構建器也能使用自變量,以便我們指定對象的具體創建方式。

class Rock {

Rock(inti) {

System.out.println("Creating Rocknumber " + i);

}

}

 

publicclass test {

publicstaticvoid main(String[] args) {

for (inti = 0; i < 10;i++)

new Rock(i);

}

}

構建器屬於一種較特殊的方法類型,因為它沒有返回值。這與 void 返回值存在著明顯的區別。對於void 返回值,盡管方法本身不會自動返回什麼,但仍然可以讓它返回另一些東西。構建器則不同,它不僅什麼也不會自動返回,而且根本不能有任何選擇。若存在一個返回值,而且假設我們可以自行選擇返回內容,那麼編譯器多少要知道如何對那個返回值作什麼樣的處理。

 

2 方法過載

在任何程序設計語言中,一項重要的特性就是名字的運用。我們創建一個對象時,會分配到一個保存區域的名字。方法名代表的是一種具體的行動。通過用名字描述自己的系統,可使自己的程序更易人們理解和修改。

我們用名字引用或描述所有對象與方法。若名字選得好,可使自己及其他人更易理解自己的代碼。 將人類語言中存在細致差別的概念“映射”到一種程序設計語言中時,會出現一些特殊的問題。這是由於聽眾根本不需要對執行的行動作任何明確的區分。人類的大多數語言都具有很強的“冗余”性,所以即使漏掉了幾個詞,仍然可以推斷出含義。我們不需要獨一無二的標識符——可從具體的語境中推論出含義。

大多數程序設計語言(特別是C)要求我們為每個函數都設定一個獨一無二的標識符。所以絕對不能用一個名為print()的函數來顯示整數,再用另一個print()顯示浮點數——每個函數都要求具備唯一的名字。

在Java 裡,另一項因素強迫方法名出現過載情況:構建器。由於構建器的名字由類名決定,所以只能有一個構建器名稱。但假若我們想用多種方式創建一個對象呢?例如,假設我們想創建一個類,令其用標准方式進行初始化,另外從文件裡讀取信息來初始化。此時,我們需要兩個構建器,一個沒有自變量(默認構建器),另一個將字串作為自變量——用於初始化對象的那個文件的名字。由於都是構建器,所以它們必須有相同的名字,亦即類名。所以為了讓相同的方法名伴隨不同的自變量類型使用,“方法過載”是非常關鍵的一項措施。同時,盡管方法過載是構建器必需的,但它亦可應用於其他任何方法,且用法非常方便。

 

示例如下:

importjava.util.*;

 

class Tree {

 

intheight;

 

Tree(){

prt("Planting aseedling");

height = 0;

}

 

Tree(inti) {

prt("Creating newTree that is "+i +" feet tall");

height =i;

}

 

void info() {

prt("Tree is " +height +" feet tall");

}

 

void info(Strings) {

prt(s +": Tree is " +height +" feet tall");

}

 

staticvoid prt(Strings) {

System.out.println(s);

}

}

 

publicclass test {

publicstaticvoid main(String[] args) {

for (inti = 0; i < 5;i++) {

Treet =new Tree(i);

t.info();

t.info("overloaded method");

}

// Overloadedconstructor:

new Tree();

}

} // /:~

輸出如下:

Creatingnew Tree that is 0 feet tall

Treeis 0 feet tall

overloadedmethod: Tree is 0 feet tall

Creatingnew Tree that is 1 feet tall

Treeis 1 feet tall

overloadedmethod: Tree is 1 feet tall

Creatingnew Tree that is 2 feet tall

Treeis 2 feet tall

overloadedmethod: Tree is 2 feet tall

Creatingnew Tree that is 3 feet tall

Treeis 3 feet tall

overloadedmethod: Tree is 3 feet tall

Creatingnew Tree that is 4 feet tall

Treeis 4 feet tall

overloadedmethod: Tree is 4 feet tall

Plantinga seedling

Tree 既可創建成一顆種子,不含任何自變量;亦可創建成生長在苗圃中的植物。為支持這種創建,共使用了兩個構建器,一個沒有自變量,另一個采用現成的高度。

4.2.1 區分過載方法

若方法有同樣的名字,Java 怎樣知道我們指的哪一個方法呢?這裡有一個簡單的規則:每個過載的方法都必須采取獨一無二的自變量類型列表。

即使自變量的順序也足夠我們區分兩個方法(盡管我們通常不願意采用這種方法,因為它會產生難以維護的代碼)

示例如下:

publicclass test {

staticvoid print(Strings,inti) {

System.out.println(

"String:" +s +

",int: " +i);

}

staticvoid print(inti, String s) {

System.out.println(

"int:" +i +

",String: " +s);

}

publicstaticvoid main(String[] args) {

print("String first", 11);

print(99,"Int first");

}

} ///:~

3 返回值過載

為什麼只有類名和方法自變量列出?為什麼不根據返回值對方法加以區分?比如對下面這兩個方法來說,雖然它們有同樣的名字和自變量,但其實是很容易區分的:

void f() {}

int f() {}

若編譯器可根據上下文(語境)明確判斷出含義,比如在 int x=f()中,那麼這樣做完全沒有問題。然而,我們也可能調用一個方法,同時忽略返回值;我們通常把這稱為“為它的副作用去調用一個方法”,因為我們關心的不是返回值,而是方法調用的其他效果。所以假如我們象下面這樣調用方法:

f();

Java 怎樣判斷f()的具體調用方式呢?而且別人如何識別並理解代碼呢?由於存在這一類的問題,所以不能根據返回值類型來區分過載的方法。

 

4 默認構建器

默認構建器是沒有自變量的。它們的作用是創建一個“空對象”。若創建一個沒有構建器的類,則編譯程序會幫我們自動創建一個默認構建器。

如下:

class Bird {

inti;

}

 

publicclass test {

publicstaticvoid main(String[] args) {

Bird nc =new Bird();// default!

}

} ///:~

new Bird(); 它的作用是新建一個對象,並調用默認構建器——即使尚未明確定義一個象這樣的構建器。若沒有它,就沒有方法可以調用,無法構建我們的對象。然而,如果已經定義了一個構建器(無論是否有自變量),編譯程序都不會幫我們自動合成一個

5 this 關鍵字

如果有兩個同類型的對象,分別叫作a 和b,那麼您也許不知道如何為這兩個對象同時調用一個f()方法:

class Banana { void f(int i) { /* ... */ }}

Banana a = new Banana(), b = new Banana();

a.f(1);

b.f(2);

 

若只有一個名叫f()的方法,它怎樣才能知道自己是為 a 還是為b 調用的呢?

為了能用簡便的、面向對象的語法來書寫代碼——亦即“將消息發給對象”,編譯器為我們完成了一些幕後工作。其中的秘密就是第一個自變量傳遞給方法f(),而且那個自變量是准備操作的那個對象的句柄。所以前述的兩個方法調用就變成了下面這樣的形式:

Banana.f(a,1);

Banana.f(b,2);

這是內部的表達形式,我們並不能這樣書寫表達式,並試圖讓編譯器接受它。但是,通過它可理解幕後到底發生了什麼事情。

 

假定我們在一個方法的內部,並希望獲得當前對象的句柄。由於那個句柄是由編譯器“秘密”傳遞的,所以沒有標識符可用。然而,針對這一目的有個專用的關鍵字:this。

this 關鍵字(注意只能在方法內部使用)可為已調用了其方法的那個對象生成相應的句柄。可象對待其他任何對象句柄一樣對待這個句柄。但要注意,假若准備從自己某個類的另一個方法內部調用一個類方法,就不必使用this。只需簡單地調用那個方法即可。當前的this 句柄會自動應用於其他方法。

this 關鍵字只能用於那些特殊的類——需明確使用當前對象的句柄。例如,假若您希望將句柄返回給當前對象,那麼它經常在return 語句中使用。

 

示例如下:

publicclass test {

privateinti = 0;

test increment() {

i++;

returnthis;

}

void print() {

System.out.println("i = " + i);

}

publicstaticvoid main(String[] args) {

test x =new test();

x.increment().increment().increment().print();

}

} ///:~

由於increment()通過this 關鍵字返回當前對象的句柄,所以可以方便地對同一個對象執行多項操作。

 

5.1 在構建器裡調用構建器(this)

若為一個類寫了多個構建器,那麼經常都需要在一個構建器裡調用另一個構建器,以避免寫重復的代碼。可用this 關鍵字做到這一點。

通常,當我們說this 的時候,都是指“這個對象”或者“當前對象”。而且它本身會產生當前對象的一個句柄。在一個構建器中,若為其賦予一個自變量列表,那麼 this 關鍵字會具有不同的含義:它會對與那個自變量列表相符的構建器進行明確的調用。這樣一來,我們就可通過一條直接的途徑來調用其他構建器。

示例如下:

publicclass Flower {

privateintpetalCount = 0;

private Strings =new String("null");

 

Flower(intpetals ) {

petalCount =petals;

System.out.println("Constructor w/int arg only, petalCount= "

+petalCount);

}

 

Flower(Stringss) {

System.out.println("Constructor w/ String arg only, s=" +ss);

s =ss;

}

 

Flower(Strings,intpetals) {

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("defaultconstructor (no args)");

}

 

void print() {

// ! this(11); // Notinside non-constructor!

System.out.println("petalCount =" + petalCount +" s = " +s);

}

 

publicstaticvoid main(String[] args) {

Flowerx =new Flower();

x.print();

}

} // /:~

輸出如下:

Constructorw/ int arg only, petalCount= 47

String& int args

defaultconstructor (no args)

petalCount= 47 s = hi

盡管可用this 調用一個構建器,但不可調用兩個。除此以外,構建器調用必須是我們做的第一件事情,否則會收到編譯程序的報錯信息。這個例子也向大家展示了this 的另一項用途。由於自變量s 的名字以及成員數據s 的名字是相同的,所以會出現混淆。為解決這個問題,可用 this.s來引用成員數據。經常都會在 Java 代碼裡看到這種形式的應用。在print()中,我們發現編譯器不讓我們從除了一個構建器之外的其他任何方法內部調用一個構建器。

 

5.2 static 的含義

理解了this 關鍵字後,我們可更完整地理解static(靜態)方法的含義。它意味著一個特定的方法沒有this。我們不可從一個 static方法內部發出對非 static方法的調用,盡管反過來說是可以的。

而且在沒有任何對象的前提下,我們可針對類本身發出對一個 static方法的調用。事實上,那正是 static方法最基本的意義。它就好象我們創建一個全局函數的等價物(在C 語言中)。除了全局函數不允許在Java中使用以外,若將一個 static方法置入一個類的內部,它就可以訪問其他static 方法以及static 字段。

有些人抱怨 static方法並不是“面向對象”的,因為它們具有全局函數的某些特點;利用static方法,我們不必向對象發送一條消息,因為不存在this。這可能是一個清楚的自變量,若您發現自己使用了大量靜態方法,就應重新思考自己的策略。然而,static 的概念是非常實用的,許多時候都需要用到它。所以至於它們是否真的“面向對象”,應該留給理論家去討論。事實上,即使Smalltalk 在自己的“類方法”裡也有類似於static的東西。

 

6 清除:收尾和垃圾收集

程序員都知道“初始化”的重要性,但通常忘記清除的重要性。

但是對於庫來說,用完後簡單地“釋放”一個對象並非總是安全的。當然,Java可用垃圾收集器回收由不再使用的對象占據的內存。現在考慮一種非常特殊且不多見的情況。假定我們的對象分配了一個“特殊”內存區域,沒有使用new。

垃圾收集器只知道釋放那些由new分配的內存,所以不知道如何釋放對象的“特殊”內存。為解決這個問題,Java提供了一個名為finalize()的方法,可為我們的類定義它。在理想情況下,它的工作原理應該是這樣的:一旦垃圾收集器准備好釋放對象占用的存儲空間,它首先調用finalize(),而且只有在下一次垃圾收集過程中,才會真正回收對象的內存。所以如果使用finalize(),就可以在垃圾收集期間進行一些重要的清除或清掃工作。

但也是一個潛在的編程陷阱,因為有些程序員(特別是在C++開發背景的)剛開始可能會錯誤認為它就是在C++中為“破壞器”(Destructor)使用的finalize()——破壞(清除)一個對象的時候,肯定會調用這個函數。有必要區分一下C++和Java 的區別,因為C++的對象肯定會被清除(排開編程錯誤的因素),而Java 對象並非肯定能作為垃圾被“收集”去。或者換句話說: 垃圾收集並不等於“破壞”!

 

Java 並未提供“破壞器”或者類似的概念,所以必須創建一個原始的方法,用它來進行這種清除。例如,假設在對象創建過程中,它會將自己描繪到屏幕上。如果不從屏幕明確刪除它的圖像,那麼它可能永遠都不會被清除。若在finalize()裡置入某種刪除機制,那麼假設對象被當作垃圾收掉了,圖像首先會將自身從屏幕上移去。但若未被收掉,圖像就會保留下來。所以要記住:我們的對象可能不會當作垃圾被收掉!

有時可能發現一個對象的存儲空間永遠都不會釋放,因為自己的程序永遠都接近於用光空間的臨界點。若程序執行結束,而且垃圾收集器一直都沒有釋放我們創建的任何對象的存儲空間,則隨著程序的退出,那些資源會返回給操作系統。這是一件好事情,因為垃圾收集本身也要消耗一些開銷。如永遠都不用它,那麼永遠也不用支出這部分開銷。

 

6.1 finalize() 用途何在

 

垃圾收集只跟內存有關!

垃圾收集器存在的唯一原因是為了回收程序不再使用的內存。所以對於與垃圾收集有關的任何活動來說,其中最值得注意的是finalize()方法,它們也必須同內存以及它的回收有關。

但這是否意味著假如對象包含了其他對象,finalize()就應該明確釋放那些對象呢?答案是否定的——垃圾收集器會負責釋放所有對象占據的內存,無論這些對象是如何創建的。它將對finalize()的需求限制到特殊的情況。在這種情況下,我們的對象可采用與創建對象時不同的方法分配一些存儲空間。

之所以要使用finalize(),看起來似乎是由於有時需要采取與Java的普通方法不同的一種方法,通過分配內存來做一些具有C 風格的事情。這主要可以通過“固有方法”來進行,它是從Java 裡調用非Java 方法的一種方式。C和C++是目前唯一獲得固有方法支持的語言。

但由於它們能調用通過其他語言編寫的子程序,所以能夠有效地調用任何東西。在非Java 代碼內部,也許能調用C 的malloc()系列函數,用它分配存儲空間。而且除非調用了free(),否則存儲空間不會得到釋放,從而造成內存“漏洞”的出現。當然,free()是一個C 和C++函數,所以我們需要在finalize()內部的一個固有方法中調用它。

 

6.2 必須執行清除

為清除一個對象,那個對象的用戶必須在希望進行清除的地點調用一個清除方法。這聽起來似乎很容易做到,但卻與 C++“破壞器”的概念稍有抵觸。在C++中,所有對象都會破壞(清除)。或者換句話說,所有對象都“應該”破壞。若將C++對象創建成一個本地對象,比如在堆棧中創建(在 Java 中是不可能的),那麼清除或破壞工作就會在“結束花括號”所代表的、創建這個對象的作用域的末尾進行。若對象是用new創建的(類似於 Java),那麼當程序員調用 C++的delete 命令時(Java 沒有這個命令),就會調用相應的破壞器。若程序員忘記了,那麼永遠不會調用破壞器,我們最終得到的將是一個內存“漏洞”,另外還包括對象的其他部分永遠不會得到清除。

相反,Java 不允許我們創建本地(局部)對象——無論如何都要使用new。但在Java 中,沒有“delete”命令來釋放對象,因為垃圾收集器會幫助我們自動釋放存儲空間。所以如果站在比較簡化的立場,我們可以說正是由於存在垃圾收集機制,所以 Java 沒有破壞器。然而,隨著以後學習的深入,就會知道垃圾收集器的存在並不能完全消除對破壞器的需要,或者說不能消除對破壞器代表的那種機制的需要(而且絕對不能直接調用finalize(),所以應盡量避免用它)。若希望執行除釋放存儲空間之外的其他某種形式的清除工作,仍然必須調用Java 中的一個方法。它等價於C++的破壞器,只是沒後者方便。

finalize()最有用處的地方之一是觀察垃圾收集的過程。

示例:

class Chair {

staticbooleangcrun = false;

staticbooleanf = false;

staticintcreated = 0;

staticintfinalized = 0;

inti;

 

Chair(){

i = ++created;

if (created == 47)

System.out.println("Created47");

}

 

protectedvoid finalize() {

if (!gcrun) {

gcrun =true;

System.out.println("Beginning tofinalize after " + created

+"Chairs have been created");

}

if (i == 47) {

System.out.println("FinalizingChair #47, "

+"Settingflag to stop Chair creation");

f =true;

}

finalized++;

if (finalized >= created)

System.out.println("All " + finalized +" finalized");

}

}

 

publicclass Flower {

publicstaticvoid main(String[] args) {

if (args.length == 0) {

System.err.println("Usage: \n" + "java Garbage before\n or:\n"

+"javaGarbage after");

return;

}

while (!Chair.f) {

new Chair();

new String("To take up space");

}

System.out.println("After allChairs have been created:\n"

+"totalcreated = "+ 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);

}

} // /:~

輸出如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

Created47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

Beginningto finalize after 220 Chairs have been created

221

FinalizingChair #47, Setting flag to stop Chair creation

222

Afterall Chairs have been created:

totalcreated = 222, total finalized = 105

gc():

runFinalization():

All222 finalized

bye!

 

上面這個程序創建了許多Chair 對象,而且在垃圾收集器開始運行後的某些時候,程序會停止創建Chair。

由於垃圾收集器可能在任何時間運行,所以我們不能准確知道它在何時啟動。因此,程序用一個名為gcrun的標記來指出垃圾收集器是否已經開始運行。利用第二個標記 f,Chair 可告訴 main()它應停止對象的生成。這兩個標記都是在 finalize()內部設置的,它調用於垃圾收集期間。

另兩個static 變量——created 以及finalized——分別用於跟蹤已創建的對象數量以及垃圾收集器已進行完收尾工作的對象數量。最後,每個Chair 都有它自己的(非static)int i,所以能跟蹤了解它具體的編號是多少。編號為47 的Chair 進行完收尾工作後,標記會設為true,最終結束Chair 對象的創建過程。

運行這個程序的時候,提供了一個命令行自變量“before”或者“after”。其中,“before”自變量會調用System.gc()方法(強制執行垃圾收集器),同時還會調用System.runFinalization()方法,以便進行收尾。

調用的runFinalizersOnExit()方法卻只有Java 1.1 及後續版本提供了對它的支持。注意可在程序執行的任何時候調用這個方法,而且收尾程序的執行與垃圾收集器是否運行是無關的。

若使用一個不是“before”或“after”的自變量(如“none”),那麼兩個收尾工

作都不會進行,而且我們會得到象下面這樣的輸出

totalcreated = 322, total finalized = 187

為強制進行收尾工作,可先調用System.gc(),再調用System.runFinalization()。這樣可清除到目前為止沒有使用的所有對象。這樣做一個稍顯奇怪的地方是在調用runFinalization()之前調用gc(),這看起來似乎與 Sun公司的文檔說明有些抵觸,它宣稱首先運行收尾模塊,再釋放存儲空間。然而,若在這裡首先調用runFinalization(),再調用gc(),收尾模塊根本不會執行。

 

7 成員初始化

Java 盡自己的全力保證所有變量都能在使用前得到正確的初始化。若被定義成相對於一個方法的“局部”變量,這一保證就通過編譯期的出錯提示表現出來。

初始化示例:

class Measurement {

booleant;

charc;

byteb;

shorts;

inti;

longl;

floatf;

doubled;

void print() {

System.out.println(

"Data typeInital 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);

}

}

 

publicclass Flower {

publicstaticvoid main(String[] args) {

Measurementd =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),沒有數據打印出來。

在一個類的內部定義一個對象句柄時,如果不將其初始化成新對象,那個句柄就會獲得一個空值。

7.1 規定初始化

如果想自己為變量賦予一個初始值,又會發生什麼情況呢?為達到這個目的,一個最直接的做法是在類內部定義變量的同時也為其賦值(注意在C++裡不能這樣做,盡管C++的新手們總“想”這樣做)

 

7.2 構建器初始化

7.2.1初始化順序

可考慮用構建器執行初始化進程。這樣便可在編程時獲得更大的靈活程度,因為我們可以在運行期調用方法和采取行動,從而“現場”決定初始化值。但要注意這樣一件事情:不可妨礙自動初始化的進行,它在構建器進入之前就會發生。

示例如下:

class Tag {

Tag(intmarker) {

System.out.println("Tag(" + marker +")");

}

}

 

class Card {

Tagt1 =new Tag(1);// Before constructor

 

Card(){

// Indicate we're inthe constructor:

System.out.println("Card()");

t3 =new Tag(33);// Re-initialize t3

}

 

Tagt2 =new Tag(2);// After constructor

 

void f() {

System.out.println("f()");

}

 

Tagt3 =new Tag(3);// At end

}

 

publicclass Flower {

publicstaticvoid main(String[] args) {

Cardt =new Card();

t.f();// Shows that construction is done

}

}

輸出如下:

Tag(1)

Tag(2)

Tag(3)

Card()

Tag(33)

f()

t3句柄會被初始化兩次,一次在構建器調用前,一次在調用期間(第一個對象會被丟棄,所以它後來可被當作垃圾收掉)。從表面看,這樣做似乎效率低下,但它能保證正確的初始化——若定義了一個過載的構建器,它沒有初始化 t3;同時在t3 的定義裡並沒有規定“默認”的初始化方式,那麼會產生什麼後果呢?

 

7.2.2靜態數據的初始化

若數據是靜態的(static),那麼同樣的事情就會發生;如果它屬於一個基本類型(主類型),而且未對其初始化,就會自動獲得自己的標准基本類型初始值;如果它是指向一個對象的句柄,那麼除非新建一個對象,並將句柄同它連接起來,否則就會得到一個空值(NULL)。

如果想在定義的同時進行初始化,采取的方法與非靜態值表面看起來是相同的。但由於static 值只有一個存儲區域,所以無論創建多少個對象,都必然會遇到何時對那個存儲區域進行初始化的問題。

示例如下:

class Bowl {

Bowl(intmarker) {

System.out.println("Bowl(" + marker +")");

}

 

void f(intmarker) {

System.out.println("f(" + marker +")");

}

}

 

class Table {

static Bowlb1 =new Bowl(1);

 

Table(){

System.out.println("Table()");

b2.f(1);

}

 

void f2(intmarker) {

System.out.println("f2(" + marker +")");

}

 

static Bowlb2 =new Bowl(2);

}

 

class Cupboard {

Bowlb3 =new Bowl(3);

static Bowlb4 =new Bowl(4);

 

Cupboard(){

System.out.println("Cupboard()");

b4.f(2);

}

 

void f3(intmarker) {

System.out.println("f3(" + marker +")");

}

 

static Bowlb5 =new Bowl(5);

}

 

publicclass Flower {

publicstaticvoid main(String[] args) {

System.out.println("Creating newCupboard() in main");

new Cupboard();

System.out.println("Creating newCupboard() in main");

new Cupboard();

t2.f2(1);

t3.f3(1);

}

 

static Tablet2 =new Table();

static Cupboardt3 =new Cupboard();

} // /:~

輸出如下:

Bowl(1)

Bowl(2)

Table()

f(1)

Bowl(4)

Bowl(5)

Bowl(3)

Cupboard()

f(2)

Creatingnew Cupboard() in main

Bowl(3)

Cupboard()

f(2)

Creatingnew 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) 執行構建器。這實際可能要求進行相當多的操作,特別是在涉及繼承的時候。

 

7.2.3明確進行的靜態初始化

Java 允許我們將其他static初始化工作劃分到類內一個特殊的“static 構建從句”(有時也叫作“靜態塊”)裡。

盡管看起來象個方法,但它實際只是一個static 關鍵字,後面跟隨一個方法主體。與其他 static初始化一樣,這段代碼僅執行一次——首次生成那個類的一個對象時,或者首次訪問屬於那個類的一個static 成員時(即便從未生成過那個類的對象)。

舉例如下:

class Cup {

Cup(intmarker) {

System.out.println("Cup(" + marker +")");

}

 

void f(intmarker) {

System.out.println("f(" + marker +")");

}

}

 

class Cups {

static Cupc1;

static Cupc2;

static {

c1 =new Cup(1);

c2 =new Cup(2);

}

 

Cups(){

System.out.println("Cups()");

}

}

 

publicclass Flower {

publicstaticvoid main(String[] args) {

System.out.println("Insidemain()");

Cups.c1.f(99);// (1)

}

 

static Cupsx =new Cups();// (2)

static Cupsy =new Cups();// (2)

} // /:~

輸出如下:

Cup(1)

Cup(2)

Cups()

Cups()

Insidemain()

f(99)

在標記為(1)的行內訪問 static 對象c1 的時候,或在行(1)標記為注釋,同時(2)行不標記成注釋的時候,用於Cups 的static初始化模塊就會運行。若(1)和(2)都被標記成注釋,則用於Cups 的static 初始化進程永遠不會發生。

 

7.2.4非靜態實例的初始化

針對每個對象的非靜態變量的初始化,Java 1.1 提供了一種類似的語法格式。

示例如下:

class Mug {

Mug(intmarker) {

System.out.println("Mug(" + marker +")");

}

void f(intmarker) {

System.out.println("f(" + marker +")");

}

}

 

publicclass Flower {

Mug c1;

Mug c2;

{

c1 =new Mug(1);

c2 =new Mug(2);

System.out.println("c1 & c2initialized");

}

Flower() {

System.out.println("Mugs()");

}

publicstaticvoid main(String[] args) {

System.out.println("Insidemain()");

Flower x =new Flower();

}

} ///:~

輸出如下:

Insidemain()

Mug(1)

Mug(2)

c1& c2 initialized

Mugs()

 

它看起來與靜態初始化從句極其相似,只是static 關鍵字從裡面消失了。為支持對“匿名內部類”的初始化,必須采用這一語法格式。

 

7.3 數組初始化

在C 中初始化數組極易出錯,而且相當麻煩。C++通過“集合初始化”使其更安全。Java 則沒有象C++那樣的“集合”概念,因為Java中的所有東西都是對象。但它確實有自己的數組,通過數組初始化來提供支持。

數組代表一系列對象或者基本數據類型,所有相同的類型都封裝到一起——采用一個統一的標識符名稱。數組的定義和使用是通過方括號索引運算符進行的([])。為定義一個數組,只需在類型名後簡單地跟隨一對空方括號即可:

int[] al;

也可以將方括號置於標識符後面,獲得完全一致的結果:

int al[];

這種格式與 C和C++程序員習慣的格式是一致的。然而,最“通順”的也許還是前一種語法,因為它指出類型是“一個 int 數組”。

編譯器不允許我們告訴它一個數組有多大。這樣便使我們回到了“句柄”的問題上。此時,我們擁有的一切就是指向數組的一個句柄,而且尚未給數組分配任何空間。為了給數組創建相應的存儲空間,必須編寫一個初始化表達式。對於數組,初始化工作可在代碼的任何地方出現,但也可以使用一種特殊的初始化表達式,它必須在數組創建的地方出現。這種特殊的初始化是一系列由花括號封閉起來的值。存儲空間的分配(等價於使用new)將由編譯器在這種情況下進行。例如:

int[] a1 = { 1,2, 3, 4, 5 };

那麼為什麼還要定義一個沒有數組的數組句柄呢?

int[] a2;

事實上在Java 中,可將一個數組分配給另一個,所以能使用下述語句:

a2 = a1;

我們真正准備做的是復制一個句柄.

示例如下:

publicclass test {

publicstaticvoid main(String[] args) {

int[]a1 = { 1, 2, 3, 4, 5 };

int[]a2;

a2 =a1;

for(inti = 0; i

a2[i]++;

for(inti = 0; i

prt("a1[" +i +"] = " +a1[i]);

}

staticvoid prt(Strings) {

System.out.println(s);

}

} ///:~

輸出如下:

a1[0]= 2

a1[1]= 3

a1[2]= 4

a1[3]= 5

a1[4] = 6

 

a1 獲得了一個初始值,而a2 沒有;a2將在以後賦值——這種情況下是賦給另一個數組。

所有數組都有一個本質成員(無論它們是對象數組還是基本類型數組),可對其進行查詢——但不是改變,從而獲知數組內包含了多少個元素。這個成員就是length。與C和C++類似,由於Java 數組從元素 0 開始計數,所以能索引的最大元素編號是“length-1”。如超出邊界,C 和C++會“默默”地接受,並允許我們胡亂使用自己的內存,這正是許多程序錯誤的根源。

然而,Java 可保留我們這受這一問題的損害,方法是一旦超過邊界,就生成一個運行期錯誤(即一個“違例”,就是Exception)。當然,由於需要檢查每個數組的訪問,所以會消耗一定的時間和多余的代碼量,而且沒有辦法把它關閉。這意味著數組訪問可能成為程序效率低下的重要原因——如果它們在關鍵的場合進行。

但考慮到因特網訪問的安全,以及程序員的編程效率,Java 設計人員還是應該把它看作是值得的。 程序編寫期間,如果不知道在自己的數組裡需要多少元素,那麼又該怎麼辦呢?此時,只需簡單地用new在數組裡創建元素。在這裡,即使准備創建的是一個基本數據類型的數組,new也能正常地工作(new不會創建非數組的基本類型)

示例如下:

import java.util.*;

 

publicclass test {

static Randomrand =new Random();

staticint pRand(intmod) {

return Math.abs(rand.nextInt()) % mod + 1;

}

publicstaticvoid main(String[] args) {

int[]a;

a =newint[pRand(20)];

prt("length of a = " +a.length);

for(inti = 0; i

prt("a["+i +"] = " +a[i]);

}

staticvoid prt(Strings) {

System.out.println(s);

}

} ///:~

輸出結果:

lengthof a = 12

a[0]= 0

a[1]= 0

a[2]= 0

a[3]= 0

a[4]= 0

a[5]= 0

a[6]= 0

a[7]= 0

a[8]= 0

a[9]= 0

a[10]= 0

a[11]= 0

 

由於數組的大小是隨機決定的(使用早先定義的pRand()方法),所以非常明顯,數組的創建實際是在運行期間進行的。除此以外,從這個程序的輸出中,大家可看到基本數據類型的數組元素會自動初始化成“空”值(對於數值,空值就是零;對於 char,它是null;而對於boolean,它卻是false)。

當然,數組可能已在相同的語句中定義和初始化了,如下所示:

int[] a = new int[pRand(20)];

若操作的是一個非基本類型對象的數組,那麼無論如何都要使用new。在這裡,我們會再一次遇到句柄問題,因為我們創建的是一個句柄數組。請大家觀察封裝器類型 Integer,它是一個類,而非基本數據類型。

再來看個例子,由於所有類最終都是從通用的根類Object 中繼承的,所以能創建一個方法,令其獲取一個 Object數組:

class A {

inti;

}

 

publicclass test {

staticvoid f(Object[]x) {

for (inti = 0; i

System.out.println(x[i]);

}

 

publicstaticvoid main(String[] args) {

f(new Object[] {new Integer(47),new test(),new Float(3.14),

new Double(11.11) });

f(new Object[] {"one","two","three" });

f(new Object[] {new A(),new A(),new A() });

}

} // /:~

輸出如下:

47

test@5e374549

3.14

11.11

one

two

three

A@e11d0a85

A@82a4d9fb

A@e4d67d99

我們對這些未知的對象並不能采取太多的操作,而且這個程序利用自動String轉換對每個 Object 做一些有用的事情。

7.4 多維數組

在Java 裡可以方便地創建多維數組,示例如下:

import java.util.*;

 

publicclass test {

static Randomrand =new Random();

 

staticint pRand(intmod) {

return Math.abs(rand.nextInt()) % mod + 1;

}

 

publicstaticvoid main(String[] args) {

int[][]a1 = { { 1, 2, 3, }, { 4, 5, 6, }, };

for (inti = 0; i

for (intj = 0; j

prt("a1[" +i +"][" +j +"] = " +a1[i][j]);

// 3-D array withfixed length:

int[][][]a2 =newint[2][2][4];

for (inti = 0; i

for (intj = 0; j

for (intk = 0; k

prt("a2[" +i +"][" +j +"][" +k +"] = " +a2[i][j][k]);

// 3-D array withvaried-length vectors:

int[][][]a3 =newint[pRand(7)][][];

for (inti = 0; i

a3[i] =newint[pRand(5)][];

for (intj = 0; j

a3[i][j] = newint[pRand(5)];

}

for (inti = 0; i

for (intj = 0; j

for (intk = 0; k

prt("a3[" +i +"][" +j +"][" +k +"] = " +a3[i][j][k]);

// Array ofnon-primitive objects:

Integer[][]a4 = { {new Integer(1),new Integer(2) },

{new Integer(3),new Integer(4) },

{new Integer(5),new Integer(6) }, };

for (inti = 0; i

for (intj = 0; j

prt("a4[" +i +"][" +j +"] = " +a4[i][j]);

Integer[][]a5;

a5 =new Integer[3][];

for (inti = 0; i

a5[i] =new Integer[3];

for (intj = 0; j

a5[i][j] = new Integer(i *j);

}

for (inti = 0; i

for (intj = 0; j

prt("a5[" +i +"][" +j +"] = " +a5[i][j]);

}

 

staticvoid prt(Strings) {

System.out.println(s);

}

} // /:~

輸出如下:

a1[0][0]= 1

a1[0][1]= 2

a1[0][2]= 3

a1[1][0]= 4

a1[1][1]= 5

a1[1][2]= 6

a2[0][0][0]= 0

a2[0][0][1]= 0

a2[0][0][2]= 0

a2[0][0][3]= 0

a2[0][1][0]= 0

a2[0][1][1]= 0

a2[0][1][2]= 0

a2[0][1][3]= 0

a2[1][0][0]= 0

a2[1][0][1]= 0

a2[1][0][2]= 0

a2[1][0][3]= 0

a2[1][1][0]= 0

a2[1][1][1]= 0

a2[1][1][2]= 0

a2[1][1][3]= 0

a3[0][0][0]= 0

a3[0][0][1]= 0

a3[0][0][2]= 0

a3[0][1][0]= 0

a3[0][1][1]= 0

a3[0][1][2]= 0

a3[0][1][3]= 0

a3[0][2][0]= 0

a3[0][3][0]= 0

a3[0][3][1]= 0

a3[0][3][2]= 0

a3[1][0][0]= 0

a3[1][0][1]= 0

a3[1][0][2]= 0

a3[1][0][3]= 0

a3[1][0][4]= 0

a3[1][1][0]= 0

a3[1][1][1]= 0

a3[1][2][0]= 0

a3[1][2][1]= 0

a3[1][2][2]= 0

a3[1][2][3]= 0

a3[2][0][0]= 0

a3[2][0][1]= 0

a3[2][0][2]= 0

a3[2][1][0]= 0

a3[2][1][1]= 0

a3[2][1][2]= 0

a3[2][1][3]= 0

a3[2][1][4]= 0

a3[2][2][0]= 0

a3[2][2][1]= 0

a3[2][2][2]= 0

a3[2][2][3]= 0

a4[0][0]= 1

a4[0][1]= 2

a4[1][0]= 3

a4[1][1]= 4

a4[2][0]= 5

a4[2][1]= 6

a5[0][0]= 0

a5[0][1]= 0

a5[0][2]= 0

a5[1][0]= 0

a5[1][1]= 1

a5[1][2]= 2

a5[2][0]= 0

a5[2][1]= 2

a5[2][2]= 4

 

用於打印的代碼裡使用了length,所以它不必依賴固定的數組大小。

第一個例子展示了基本數據類型的一個多維數組。我們可用花括號定出數組內每個矢量的邊界:

int[][] a1 = {

{ 1, 2, 3, },

{ 4, 5, 6, },

};

每個方括號對都將我們移至數組的下一級。

第二個例子展示了用new分配的一個三維數組。在這裡,整個數組都是立即分配的:

int[][][] a2 = new int[2][2][4];

第三個例子卻向大家揭示出構成矩陣的每個矢量都可以有任意的長度:

int[][][] a3 = new int[pRand(7)][][];

對於第一個 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裡置了一個的值。

 

8 總結

作為初始化的一種具體操作形式,構建器應使大家明確感受到在語言中進行初始化的重要性。與 C++的程序設計一樣,判斷一個程序效率如何,關鍵是看是否由於變量的初始化不正確而造成了嚴重的編程錯誤(臭蟲)。這些形式的錯誤很難發現,而且類似的問題也適用於不正確的清除或收尾工作。由於構建器使我們能保證正確的初始化和清除(若沒有正確的構建器調用,編譯器不允許對象創建),所以能獲得完全的控制權和安全性。

在C++中,與“構建”相反的“破壞”(Destruction)工作也是相當重要的,因為用new 創建的對象必須明確地清除。在Java中,垃圾收集器會自動為所有對象釋放內存,所以 Java 中等價的清除方法並不是經常都需要用到的。如果不需要類似於構建器的行為,Java 的垃圾收集器可以極大簡化編程工作,而且在內存的管理過程中增加更大的安全性。有些垃圾收集器甚至能清除其他資源,比如圖形和文件句柄等。然而,垃圾收集器確實也增加了運行期的開銷。但這種開銷到底造成了多大的影響卻是很難看出的,因為到目前為止,Java 解釋器的總體運行速度仍然是比較慢的。隨著這一情況的改觀,我們應該能判斷出垃圾收集器的開銷是否使Java 不適合做一些特定的工作(其中一個問題是垃圾收集器不可預測的性質)。

由於所有對象都肯定能獲得正確的構建,所以同這兒講述的情況相比,構建器實際做的事情還要多得多。特別地,當我們通過“創作”或“繼承”生成新類的時候,對構建的保證仍然有效,而且需要一些附加的語法來提供對它的支持。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved