隨著電腦革命的發展,“不安全”的編程方式已逐漸成為編程代價高昂的主因之一。 C++引入了構造囂(constructor)的概念,這是一個在創建對象時被自動調用的特殊方法。Java中也採用了構造器,並額外提供了“垃圾回收器”。對於不再使用的記憶體資源,垃圾回收器能自動將其釋放。 5.1 用構造器確保初 ...
隨著電腦革命的發展,“不安全”的編程方式已逐漸成為編程代價高昂的主因之一。
C++引入了構造囂(constructor)的概念,這是一個在創建對象時被自動調用的特殊方法。Java中也採用了構造器,並額外提供了“垃圾回收器”。對於不再使用的記憶體資源,垃圾回收器能自動將其釋放。
5.1 用構造器確保初始化
//:initialization/SimpleConstructor.java //Demonstration of a simple constructor. class Rock { Rock() { System.out.print("Rock "); } } public class SimpleConstructor { public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Rock(); } } }/*Output Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock *///:~
在創建對象時:new Rock();
將會為對象分配存儲空間,並調用相應的構造器。這就確保了在你能操作對象之前,它已經被恰當地初始化了。
請註意,由於構造器的名稱必須與類名完全相同,所以“每個方法首字母小寫”的編碼風格並不適用於構造器。
//:initialization/SimpleConstructor2.java class Rock2 { Rock2(int i) { System.out.print("Rock2 "+i+" "); } } public class SimpleConstructor2 { public static void main(String[] args) { for (int i = 0; i < 8; i++) { new Rock2(i); } } }/*Output Rock2 0 Rock2 1 Rock2 2 Rock2 3 Rock2 4 Rock2 5 Rock2 6 Rock2 7 *///:~
有了構造器形式參數,就可以在初始化對象時提供實際參數。例知,假設類Tree有一個構造器,它接受一個整型變數來表示樹的高度,就可以這樣創建一個Tree對象:
Tree t = new Tree(12); //12-foot tree
如果Tree(int)是Tree類中唯一的構造器,那麼編譯器將不會允許你以其他任何方式創建Tree對象。
構造器有助於減少錯誤,並使代碼更易於閱讀。從概念上講,“初始化”與“創建”是彼此獨立的,然而在上面的代碼中,你卻找不到對initialize()方法的明確調用。在Java中,“初始化”和“創建”捆綁在一起,兩者不能分離。
- 練習1:(1)創建一個類,它包含一個未初始化的String引用。驗證該引用被Java初始化成了null。
- 練習2:(2)創建一個類,它包含一個在定義時就被初始化了的String域,以及另一個通過構造器初始化的String域。這兩種方式有何差異?
5.2 方法重載
當創建一個對象時,也就給此對象分配到的存儲空間取了一個名字。所謂方法則是給某個動作取的名字。
大多數程式設計語言(尤其是C)要求為每個方法(在這些語言中經常稱為函數)都提供一個獨一無二的標識符。
所以絕不能用名為print()的函數顯示了整數之後,又用一個名為print()的函數顯示浮點數——每個函數都要有唯一的名稱。
構造器是強制重載方法名的另一個原因。既然構造器的名字已經由類名所決定,就只能有一個構造器名。那麼要想用多種方式創建一個對象該怎麼辦呢?假設你要創建一個類,既可以用標準方式進行初始化,也可以從文件里讀取信息來初始化。這就需要兩個構造器:一個預設構造器,另一個取字元串作為形式參數——該字元串表示初始化對象所需的文件名稱。由於都是構造器,所以它們必須有相同的名字,即類名。為了讓方法名相同而形式參數不同的構造器同時存在,必須用到方法重載。
下麵這個例子同時示範了重載的構造器和重載的方法:
class Tree { int height; public Tree() { height = 0; System.out.println("種植樹苗"); } public Tree(int initialHeight) { height = initialHeight; System.out.println("新創建了一顆 " + height + " 高的樹"); } void info() { System.out.println("本樹高為 " + height); } void info(String s) { System.out.println(s + ":本樹高為 " + height); } } public class Overloading { public static void main(String[] args) { for (int i = 0; i < 3; i++) { Tree t = new Tree(i); t.info(); t.info("重載的方法"); } //重載構造器 new Tree(); } }/*Output 新創建了一顆 0 高的樹 本樹高為 0 重載的方法:本樹高為 0 新創建了一顆 1 高的樹 本樹高為 1 重載的方法:本樹高為 1 新創建了一顆 2 高的樹 本樹高為 2 重載的方法:本樹高為 2 種植樹苗 *///:~
5.2.1 區分重載方法
規則很簡單:每個重載的方法都必須有一個獨一無二的參數類型列表。
甚至參數順序的不同也足以區分兩個方法。不過,一般情況下別這麼做,因為這會使代碼
難以維護:
public class OverloadingOrder { static void f(String s, int i) { System.out.println("String: " + s + ", int: " + i); } static void f(int i, String s) { System.out.println("int: " + i + ", String: " + s); } public static void main(String[] args) { f("String first", 11); f(99, "int first"); } }/*Output String: String first, int: 11 int: 99, String: int first *///:~
上例中兩個f()方法雖然聲明瞭相同的參數,但順序不同,因此得以區分。
5.2.2 涉及基本類型的重載
基本類型能從一個“較小一的類型自動提升至一個“較大”的類型,此過程一旦牽涉到重載,可能會造成一些混淆。以下例子說明瞭將基本類型傳遞給重載方法時發生的情況:
public class PrimitiveOverloading { //*******************f1***************// void f1(char x) { System.out.print("f1(char) "); } void f1(byte x) { System.out.print("f1(byte) "); } void f1(short x) { System.out.print("f1(short) "); } void f1(int x) { System.out.print("f1(int) "); } void f1(long x) { System.out.print("f1(long) "); } void f1(float x) { System.out.print("f1(float) "); } void f1(double x) { System.out.print("f1(double) "); } //********************f2**************// void f2(byte x) { System.out.print("f2(byte) "); } void f2(short x) { System.out.print("f2(short) "); } void f2(int x) { System.out.print("f2(int) "); } void f2(long x) { System.out.print("f2(long) "); } void f2(float x) { System.out.print("f2(float) "); } void f2(double x) { System.out.print("f2(double) "); } //*******************f3***************// void f3(short x) { System.out.print("f3(short) "); } void f3(int x) { System.out.print("f3(int) "); } void f3(long x) { System.out.print("f3(long) "); } void f3(float x) { System.out.print("f3(float) "); } void f3(double x) { System.out.print("f3(double) "); } //********************f4***************// void f4(int x) { System.out.print("f4(int) "); } void f4(long x) { System.out.print("f4(long) "); } void f4(float x) { System.out.print("f4(float) "); } void f4(double x) { System.out.print("f4(double) "); } //********************f5**************// void f5(long x) { System.out.print("f5(long) "); } void f5(float x) { System.out.print("f5(float) "); } void f5(double x) { System.out.print("f5(double) "); } //********************f6**************// void f6(float x) { System.out.print("f6(float) "); } void f6(double x) { System.out.print("f6(double) "); } //********************f7**************// void f7(double x) { System.out.print("f7(double) "); } void testConstVal() { System.out.print("5: "); f1(5); f2(5); f3(5); f4(5); f5(5); f6(5); f7(5); System.out.println(); } void testChar() { char x = 'x'; System.out.print("char: "); f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x); System.out.println(); } void testByte() { byte x = 0; System.out.print("byte: "); f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x); System.out.println(); } void testShort() { short x = 0; System.out.print("short: "); f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x); System.out.println(); } void testInt() { int x = 0; System.out.print("int: "); f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x); System.out.println(); } void testLong() { long x = 0; System.out.print("long: "); f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x); System.out.println(); } void testFloat() { float x = 0; System.out.print("float: "); f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x); System.out.println(); } void testDouble() { double x = 0; System.out.print("double: "); f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x); System.out.println(); } public static void main(String[] args) { PrimitiveOverloading p = new PrimitiveOverloading(); p.testConstVal(); p.testChar(); p.testByte(); p.testShort(); p.testInt(); p.testLong(); p.testFloat(); p.testDouble(); } }/*Output 5: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double) short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double) int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double) float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double) double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double) *///:~
你會發現常數值5被當作int值處理,所以如果有某個重載方法接受int型參數,它就會被調用。至於其他情況,如果傳入的數據類型(實際參數類型)小於方法中聲明的形式參數類型,實際數據類型就會被提升。 char型略有不同,如果無法找到恰好接受char參數的方法,就會把char直接提升至int型。
如果傳入的實際參數大於重載方法聲明的形式參數,會出現什麼情況呢?修改上述程式,就能得到答案。
public class Demotion() { //*******************f1***************// void f1(char x) { System.out.print("f1(char) "); } void f1(byte x) { System.out.print("f1(byte) "); } void f1(short x) { System.out.print("f1(short) "); } void f1(int x) { System.out.print("f1(int) "); } void f1(long x) { System.out.print("f1(long) "); } void f1(float x) { System.out.print("f1(float) "); } void f1(double x) { System.out.print("f1(double) "); } //********************f2**************// void f2(char x) { System.out.print("f2(char) "); } void f2(byte x) { System.out.print("f2(byte) "); } void f2(short x) { System.out.print("f2(short) "); } void f2(int x) { System.out.print("f2(int) "); } void f2(long x) { System.out.print("f2(long) "); } void f2(float x) { System.out.print("f2(float) "); } //*******************f3***************// void f3(char x) { System.out.print("f3(char) "); } void f3(byte x) { System.out.print("f3(byte) "); } void f3(short x) { System.out.print("f3(short) "); } void f3(int x) { System.out.print("f3(int) "); } void f3(long x) { System.out.print("f4(long) "); } //********************f4***************// void f4(char x) { System.out.print("f4(char) "); } void f4(byte x) { System.out.print("f4(byte) "); } void f4(short x) { System.out.print("f4(short) "); } void f4(int x) { System.out.print("f4(int) "); } //********************f5**************// void f5(char x) { System.out.print("f5(char) "); } void f5(byte x) { System.out.print("f5(byte) "); } void f5(short x) { System.out.print("f5(short) "); } //********************f6**************// void f6(char x) { System.out.print("f6(char) "); } void f6(byte x) { System.out.print("f6(byte) "); } //********************f7**************// void f7(char x) { System.out.print("f7(char) "); } void testDouble() { double x = 0; System.out.print("double argument: "); f1(x); f2((float) x); f3((long) x); f4((int) x); f5((short) x); f6((byte) x); f7((char) x); System.out.println(); } public static void main(String[] args) { Demotion d = new Demotion(); d.testDouble(); } }/*Output double argument: f1(double) f2(float) f4(long) f4(int) f5(short) f6(byte) f7(char) *///:~
在這裡,方法接受較小的基本類型作為參數。如果傳入的實際參數較大,就得通過類型轉換來執行窄化轉換。如果不這樣做,編譯器就會報錯。
5.2.3 以返回值區分重載方法
void f() { } int f() { return 1; }
f();
此時Java如何才能判斷該調用哪一個f()呢?別人該如何理解這種代碼呢?因此,根據方法的返回值來區分重載方法是行不通的。
5.3 預設構造器
如果你寫的類中沒有構造器,則編譯器會自動幫你創建一個預設構造器。
//: initialization/DefaultConstructor.java class Bird { } public class DefaultConstructor { public static void main (String[] args) { Brid b = new Bird();//Default } }///:~
表達式 new Bird()
行創建了一個新對象,並調用其預設構造器——即使你沒有明確定義它。沒有它的話,就沒有方法可調用,就無法創建對象。但是,如果已經定義了一個構造器(無論是否有參數),編譯器就不會幫你自動創建預設構造器:
//: initialization/NoSynthesis.java class bird2 { Bird2(int i) { } Bird2(double d) { } } public class NoSynthesis { public static、void main(String[] args) { //! Bird2 b = new Bird2(); // No default Bird2 b2=new Bird2(1); Bird2 b3=new Bird2(1.0); } } ///:~
要是你這樣寫:new Bird2()
編譯器就會報錯:沒有找到匹配的構造器。
- 練習3:(1)創建一個帶預設構造器(即無參構造器)的類,在構造器中列印一條消息。為這個類創建一個對象。
- 練習4:(1)為前一個練習中的類添加一個重載構造器,令其接受一個字元串參數,併在構造器中把你自己的消息和接收的參數一起列印出來。
- 練習5:(2)創建一個名為Dog的類,它具有重載的bark()方法。此方法應根據不同的基本數據類型進行重載,並根據被調用的版本,列印出不同類型的狗吠(barking)、咆哮(howling)等信息。編寫main()來調用所有不同版本的方法。
- 練習6:(1)修改前一個練習的程式,讓兩個重載方法各自接受兩個類型的不同的參數,但二者順序相反。驗證其是否工作。
- 練習7:(1)創建一個沒有構造器的類,併在main()中創建其對象,用以驗證編譯器是否真的自動加入了預設構造器。
5.4 this關鍵字
如果有同一類型的兩個對象,分別是a和b。你可能想知道,如何才能讓這兩個對象都能調用peel()方法呢:
public class BananaPeel { public static void main(String[] args)I { Banana a = new Banana(),b = new Banana(); a.peel(1); b.peel(2); } }///:~
如果只有一個peel()方法,它如何知道是被a還是被b所調用的呢?
它暗自把“所操作對象的引用”作為第一個參數傳遞給peel()。所以上述兩個方法的調用就變成了這樣:
Banana.peel(a,1);
Banana.peel(b,2);
this關鍵字只能在方法內部使用,表示對“調用方法的那個對象”的引用。this的用法和其他對象引用並無不同。但要註意,如果在方法內部調用同一個類的另一個方法,就不必使用this,直接調用即可。當前方法中的this引用會自動應用於同一類中的其他方法。所以可以這樣寫代碼:
public class Apricot { void pick () { /*...*/ } void pit() { pick(); /*...*/ } }///:~
在pit()內部,你可以寫this.pick(),但無此必要。編譯器能幫你自動添加。只有當需要明確指出對當前對象的引用時,才需要使用this關鍵字。例如,當需要返回對當前對象的引用時,就常常在return語句里這樣寫:
public class Leaf { int i = 0; Leaf increment() { i++; return this; } void print() { System.out.println("i = "+i); } public static void main(String[] args) { Leaf l = new Leaf(); l.increment().increment().increment().print(); } }/*Output i = 3 ///:~
由於increment()通過this關鍵字返回了對當前對象的引用,所以很容易在一條語句里對同一個對象執行多次操作。
this關鍵字對於將當前對象傳遞給其他方法也很有用:
class Person { public void eat(Apple apple) { Apple peeled = apple.getPeeled(); System.out.println("Yummy"); } } class Peeler { static Apple peel(Apple apple) { //...remove peel return apple; } } class Apple { Apple getPeeled() { return Peeler.peel(this); } } public class PassingThis { public static void main(String[] args) { new Person().eat(new Apple()); } }/*Output Yummy *///:~
Apple需要調用Peeler.peel()方法,它是一個外部的工具方法,將執行由於某種原因而必須放在Apple外部的操作(也許是因為該外部方法要應用於許多不同的類,而你卻不想重覆這些代碼)。為了將其自身傳遞給外部方法,Apple必須使用this關鍵字。
- 練習8:(1)編寫具有兩個方法的類,在第一個方法內調用第二個方法兩次:第一次調用時不使用this關鍵字,第二次調用時使用this關鍵字——這裡只是為了驗證它是起作用的,你不應該在實踐中使用這種方式。
5.4.1 在構造器中調用構造器
可能為一個類寫了多個構造器,有時可能想在一個構造器中調用另一個構造器,以避免重覆代碼。可用this關鍵字做到達一點。
通常寫this的時候,都是指“這個對象”或者“當前對象”,而且它本身表示對當前對象的引用。在構造器中,如果為this添加了參數列表,那麼就有了不同的含義。這將產生對符合此參數列表的某個構造器的明確調用,這樣,調用其他構造器就有了直接的途徑:
class Flower { int petalCount = 0; String s = "initial value"; Flower(int petals) { petalCount = petals; System.out.println("構造器 w/ int arg only,petalCount =" + petalCount); } Flower(String ss) { s = ss; System.out.println("構造器 w/ String arg only,s =" + s); } Flower(String s, int petals) { this(petals); //! this(s);//不能調用兩次構造器 this.s = s; System.out.println("String and int arg"); } Flower() { this("hi", 47); System.out.println("預設構造器(無參)"); } void printPetalCount() { //! this(11); //不要在非構造方法里使用 System.out.println("petalCount=" + petalCount + " s=" + s); } public static void main(String[] args) { Flower f = new Flower(); f.printPetalCount(); } }/*Output 構造器 w/ int arg only,petalCount =47 String and int arg 預設構造器(無參) petalCount=47 s=hi *///:~
構造器Flower(String s,int petals)表明:儘管可以用this調用一個構造器,但卻不能調用兩個。此外,必須將構造器調用置於最起始處,否則編譯器會報錯。 這個例子也展示了this的另一種用法。由於參數s的名稱和數據成員s的名字相同,所以會產生歧義。使用this.s來代表數據成員就能解決這個問題。在Java程式代碼中經常出現這種寫法,本書中也常這麼寫。 printPetalCount()方法表明,除構造器之外,編譯器禁止在其他任何方法中調用構造器。
- 練習9:(1)編寫具有兩個(重載)構造器的類,併在第一個構造器中通過this調用第二個構造器。
5.4.2 static的含義
在static方法的內部不能調用非靜態方法,反過來倒是可以的。(只會創建一次)
5.5 清理:終結處理和垃圾回收
Java有垃圾回收器負責回收無用對象占據的記憶體資源。但也有特殊情況:假定你的對象(並非使用new)獲得了一塊“特殊”的記憶體區域,由於垃圾回收器只知道釋放那些經由new分配的記憶體,所以它不知道該如何釋放該對象的這塊“特殊”記憶體。為了應對這種情況,Java允許在類中定義一個名為finalize()的方法。它的工作原理“假定”是這樣的:一旦垃圾回收器準備好釋放對象占用的存儲空間,將首先調用其finalize()方法,並且在下一次垃圾回收動作發生時,才會真正回收對象占用的記憶體。所以要是你打算用finalize(),就能在垃圾回收時刻做一些重要的清理工作。
在C++中,對象一定會被銷毀(如果程式中沒有缺陷的話);而Java里的對象卻並非總是被垃圾回收。或者換句話說:
- 對象可能不被垃圾回收。
- 垃圾回收並不等於“析構”。
- 垃圾回收只與記憶體有關。
Java並未提供“析構函數”或相似的概念,要做類似的清理工作,必須自己動手創建一個執行清理工作的普通方法。
也許你會發現,只要程式沒有瀕臨存儲空間用完的那一刻,對象占用的空間就總也得不到釋放。如果程式執行結束,並且垃圾回收器一直都沒有釋放你創建的任何對象的存儲空間,則隨著程式的退出,那些資源也會全部交還給操作系統。這個策略是恰當的,因為垃圾回收本身也有開銷,要是不使用它,那就不用支付這部分開銷了。
5.5.1 finalize()的用途何在
讀者或許已經明白了不要過多地使用finalize()的道理了 。對,它確實不是進行普通的清理工作的合適場所。那麼,普通的清理工作應該在哪裡執行呢?
5.5.2 你必須實施清理
Java不允許創建局部對象,必須使用new創建對象。在Java中,也沒有用於釋放對象的delete,因為垃圾回收器會幫助你釋放存儲空間。甚至可以膚淺地認為,正是由於垃圾收集機制的存在,使得Java沒有析構函數。
無論是“垃圾回收”還是“終結”,都不保證一定會發生。如果Java虛擬機(JVM)並未面臨記憶體耗盡的情形,它是不會浪費時間去執行垃圾回收以恢復記憶體的。
5.5.3 終結條件
以下是個簡單的例子,示範了fifinalize()可能的使用方式:
class Book { boolean checkedOut = false; Book(boolean checkedOut) { this.checkedOut = checkedOut; } void checkIn() { checkedOut = false; } @Override protected void finalize() throws Throwable { if (checkedOut) System.out.println("ERROR: checked out"); //通常情況下,你也會這麼做: super.finalize(); //調用基類方法 } public static void main(String[] args) { Book novel = new Book(true); //適當的清理 novel.checkIn(); //作為參考,故意忘了清理 new Book(true); //垃圾收集和終結 System.gc(); } }/*Output ERROR: checked out *///:~
System.gc()用於強制進行終結動作。即使不這麼做,通過重覆地執行程式(假設程式將分配大量的存儲空間而導致垃圾回收動作的執行),最終也能找出錯誤的book對象。
- 練習10:(2)編寫具有finalize()方法的類,併在方法中列印消息。在main()中為該類創建一個對象。試解釋這個程式的行為。
- 練習11:(4)修改前一個練習的程式,讓你的finalize()總會被調用。
- 練習12:(4)編寫名為Tank的類,此類的狀態可以是“滿的”或“空的”。其終結條件是:對象被清理時必須處於空狀態。請編寫finalize()以檢驗終結條件是否成立。在main()中測試Tank可能發生的幾種使用方式。
5.5.4 垃圾回收器如何工作
在以前所用過的程式語言中,在堆上分配對象的代價十分高昂,因此讀者自然會覺得Java中所有對象(基本類型除外),都在堆上分配的方式也非常高昂。然而,垃圾回收器對於提高對象的創建速度,卻具有明顯的效果。聽起來很奇怪——存儲空間的釋放竟然會影響存儲空間的分配,但這確實是某些Java虛擬機的工作方式。這也意味著,Java從堆分配空間的速度,可以和其他語言從堆棧上分配空間的速度相媲美。
打個比方,你可以把C++里的堆想像成一個院子,裡面每個對象都負責管理自己的地盤。一段時間以後,對象可能被銷毀,但地盤必須加以重用。在某些Java虛擬機中,堆的實現截然不同:它更像一個傳送帶,每分配一個新對象,它就往前移動一格。這意味著對象存儲空間的分配速度非常快。Java的“堆指針”只是簡單地移動到尚未分配的區域,其效率比得上C++在堆棧上分配空間的效率。當然,實際過程中在簿記工作方面還有少量額外開銷,但比不上查找可用空間開銷大。
讀者也許已經意識到了,Java中的堆未必完全像傳送帶那樣工作。要真是那樣的話,勢必會導致頻繁的記憶體頁面調度——將其移進移出硬碟,因此會顯得需要擁有比實際需要更多的記憶體。頁面調度會顯著地影響性能,最終,在創建了足夠多的對象之後,記憶體資源將耗盡。其中的秘密在於垃圾回收器的介入。當它工作時,將一面回收空間,一面使堆中的對象緊湊排列,這樣“堆指針”就可以很容易移動到更靠近傳送帶的開始處,也就儘量避免了頁面錯誤。通過垃圾回收器對對象重新排列,實現了一種高速的、有無限空間可供分配的堆模型。
要想更好地理解Java中的垃圾回收,先瞭解其他系統中的垃圾回收機制將會很有幫助。引用記數是一種簡單但速度很慢的垃圾回收技術。每個對象都含有一個引用記數器,當有引用連接至對象時,引用計數加1。當引用離開作用域或被置為null時,引用計數減1。雖然管理引用記數的開銷不大,但這項開銷在整個程式生命周期中將持續發生。垃圾回收器會在含有全部對象的列表上遍歷,當發現某個對象的引用計數為0時,就釋放其占用的空間(但是,引用記數模式經常會在記數值變為0時立即釋放對象)。這種方法有個缺陷,如果對象之間存在迴圈引用,可能會出現“對象應該被回收,但引用計數卻不為零”的情況。對垃圾回收器而言,定位這樣的交互自引用的對象組所需的工作量極大。引用記數常用來說明垃圾收集的工作方式,但似乎從未被應用於任何一種Java虛擬機實現中。
在一些更快的模式中,垃圾回收器並非基於引用記數技術。它們依據的思想是:對任何“活”的對象,一定能最終追溯到其存活在堆棧或靜態存儲區之中的引用。這個引用鏈條可能會穿過數個對象層次。由此,如果從堆棧和靜態存儲區開始,遍歷所有的引用,就能找到所有“活”的對象。對於發現的每個引用,必須追蹤它所引用的對象,然後是此對象包含的所有引用,如此反覆進行,直到“根源於堆棧和靜態存儲區的引用”所形成的網路全部被訪問為止。你所訪問過的對象必須都是“活”的。註意,這就解決了“交互自引用的對象組”的問題——這種現象根本不會被髮現,因此也就被自動回收了。
在這種方式下,Java虛擬機將採用一種自適應的垃圾回收技術。至於如何處理找到的存活對象,取決於不同的Java虛擬機實現。有一種做法名為停止一複製(stop-and-copy)。顯然這意味著,先暫停程式的運行(所以它不屬於後臺回收模式),然後將所有存活的對象從當前堆複製到另一個堆,沒有被覆制的全部都是垃圾。當對象被覆制到新堆時,它們是一個挨著一個的,所以新堆保持緊湊排列,然後就可以按前述方法簡單、直接地分配新空間了。
當把對象從一處搬到另一處時,所有指向它的那些引用都必須修正。位於堆或靜態存儲區的引用可以直接被修正,但可能還有其他指向這些對象的引用,它們在遍歷的過程中才能被找到(可以想像成有個表格,將舊地址映射至新地址)。
對於這種所謂的“複製式回收器”而言,效率會降低,這有兩個原因。首先,得有兩個堆,然後得在這兩個分離的堆之間來回搗騰,從而得維護比實際需要多一倍的空間。某些Java虛擬機對此問題的處理方式是,按需從堆中分配幾塊較大的記憶體,複製動作發生在這些大塊記憶體之間。
第二個問題在於複製。程式進入穩定狀態之後,可能只會產生少量垃圾,甚至沒有垃圾。儘管如此,複製式回收器仍然會將所有記憶體自一處複製到另一處,這很浪費。為了避免這種情形,一些Java虛擬機會進行檢查:要是沒有新垃圾產生,就會轉換到另一種工作模式(即“自適應”)。這種模式稱為標記一清掃(mark-and-sweep),Sun公司早期版本的Java虛擬機使用了這種技術。對一般用途而言,“標記一清掃”方式速度相當慢,但是當你知道只會產生少量垃圾甚至不會產生垃圾時,它的速度就很快了。
“標記一清掃”所依據的思路同樣是從堆棧和靜態存儲區出發,遍歷所有的引用,進而找出所有存活的對象。每當它找到一個存活對象,就會給對象設一個標記,這個過程中不會回收任何對象。只有全部標記工作完成的時候,清理動作才會開始。在清理過程中,沒有標記的對象將被釋放,不會發生任何複製動作。所以剩下的堆空間是不連續的,垃圾回收器要是希望得到連續空間的話,就得重新整理剩下的對象。
“停止一複製”的意思是這種垃圾回收動作不是在後臺進行的。相反,垃圾回收動作發生的同時,程式將會被暫停。在Sun公司的文檔中會發現,許多參考文獻將垃圾回收視為低優先順序的後臺進程,但事實上垃圾回收器在Sun公司早期版本的Java虛擬機中並非以這種方式實現的。當可用記憶體數量較低時,Sun版本的垃圾回收器會暫停運行程式,同樣,“標記一清掃”工作也必須在程式暫停的情況下才能進行。
如前文所述,在這裡所討論的Java虛擬機中,記憶體分配以較大的“塊”為單位。如果對象較大,它會占用單獨的塊。嚴格來說,“停止一複製”要求在釋放舊有對象之前,必須先把所有存活對象從舊堆複製到新堆,這將導致大量記憶體複製行為。有了塊之後,垃圾回收器在回收的時候就可以往廢棄的塊里拷貝對象了。每個塊都用相應的代數(generation count)來記錄它是否還存活。通常,如果塊在某處被引用,其代數會增加。垃圾回收器將對上次回收動作之後新分配的塊進行整理。這對處理大量短命的臨時對象很有幫助。垃圾回收器會定期進行完整的清理動作——大型對象仍然不會被覆制(只是其代數會增加),內含小型對象的那些塊則被覆制並整理。Java虛擬機會進行監視,如果所有對象都很穩定,垃圾回收器的效率降低的話,就切換到 “標記一清掃”方式;同樣,Java虛擬機會跟蹤“標記一清掃”的效果,要是堆空間出現很多碎片,就會切換回“停止一複製”方式。這就是“自適應”技術,你可以給它個羅嗦的稱呼:“自適應的、分代的、停止一複製、標記一清掃”式垃圾回收器。
Java虛擬機中有許多附加技術用以提升速度。尤其是與載入器操作有關的,被稱為“即時”(Just-In-Time,JIT)編譯器的技術。這種技術可以把程式全部或部分翻譯成本地機器碼(這本來是Java虛擬機的工作),程式運行速度因此得以提升。當需要裝載某個類(通常是在為該類創建第一個對象)時,編譯器會先找到其.class文件,然後將該類的位元組碼裝入記憶體。此時,有兩種方案可供選擇。一種是就讓即時編譯器編譯所有代碼。但這種做法有兩個缺陷:這種載入動作散落在整個程式生命周期內,累加起來要花更多時間.並且會增加可執行代碼的長度(位元組碼要比即時編譯器展開後的本地機器碼小很多),這將導致頁面調度,從而降低程式速度。另一種做法稱為惰性評估(lazy evaluation),意思是即時編譯器只在必要的時候才編譯代碼。這樣,從不會被執行的代碼也許就壓根不會被JIT所編譯。新版JDK中的Java HotSpot技術就採用了類似方法,代碼每次被執行的時候都會做一些優化,所以執行的次數越多,它的速度就越快。
5.6 成員初始化
Java儘力保證:所有變數在使用前都能得到恰當的初始化。對於方法的局部變數,Java以編譯時錯誤的形式來貫徹這種保證。所以如果寫成:
void f() { int i; i++;//錯誤,變數i可能沒有被初始化 }
就會得到一條出錯消息,告訴你i可能尚未初始化。當然,編譯器也可以為i賦一個預設值,但是未初始化的局部變數更有可能是程式員的疏忽,所以採用預設值反而會掩蓋這種失誤。因此強製程序員提供一個初始值,往往能夠幫助找出程式里的缺陷。
要是類的數據成員(即欄位)是基本類型,情況就會變得有些不同。正如在“一切都是對象”一章中所看到的,類的每個基本類型數據成員保證都會有一個初始值。下麵的程式可以驗證這類情況,並顯示它們的值:
public class InitialValues { boolean t; char c; byte b; short s; int i; long l; float f; double d; InitialValues iv; void