1. 程式人生 > >final關鍵字(thinking in java)

final關鍵字(thinking in java)

final關鍵字
由於語境(應用環境)不同,final關鍵字的含義可能會稍微產生一些差異。但它最一般的意思就是宣告“這個東西不能改變”。之所以要禁止改變,可能是考慮到兩方面的因素:設計或效率。由於這兩個原因頗有些區別,所以也許會造成final關鍵字的誤用。
在接下去的小節裡,我們將討論final關鍵字的三種應用場合:資料、方法以及類。

6.8.1 final資料
許多程式設計語言都有自己的辦法告訴編譯器某個資料是“常數”。常數主要應用於下述兩個方面:
(1) 編譯期常數,它永遠不會改變
(2) 在執行期初始化的一個值,我們不希望它發生變化
對於編譯期的常數,編譯器(程式)可將常數值“封裝”到需要的計算過程裡。也就是說,計算可在編譯期間提前執行,從而節省執行時的一些開銷。在Java中,這些形式的常數必須屬於基本資料型別(Primitives),而且要用final關鍵字進行表達。在對這樣的一個常數進行定義的時候,必須給出一個值。
無論static還是final欄位,都只能儲存一個數據,而且不得改變。
若隨同物件控制代碼使用final,而不是基本資料型別,它的含義就稍微讓人有點兒迷糊了。對於基本資料型別,final會將值變成一個常數;但對於物件控制代碼,final會將控制代碼變成一個常數。進行宣告時,必須將控制代碼初始化到一個具體的物件。而且永遠不能將控制代碼變成指向另一個物件。然而,物件本身是可以修改的。Java對此未提供任何手段,可將一個物件直接變成一個常數(但是,我們可自己編寫一個類,使其中的物件具有“常數”效果)。這一限制也適用於陣列,它也屬於物件。
下面是演示final欄位用法的一個例子:

[code]//: FinalData.java
// The effect of final on fields

class Value {
int i = 1;
}

public class FinalData {
// Can be compile-time constants
final int i1 = 9;
static final int I2 = 99;
// Typical public constant:
public static final int I3 = 39;
// Cannot be compile-time constants:
final int i4 = (int)(Math.random()*20);
static final int i5 = (int)(Math.random()*20);

Value v1 = new Value();
final Value v2 = new Value();
static final Value v3 = new Value();
//! final Value v4; // Pre-Java 1.1 Error:
// no initializer
// Arrays:
final int[] a = { 1, 2, 3, 4, 5, 6 };

public void print(String id) {
System.out.println(
id + ": " + "i4 = " + i4 +
", i5 = " + i5);
}
public static void main(String[] args) {
FinalData fd1 = new FinalData();
//! fd1.i1++; // Error: can't change value
fd1.v2.i++; // Object isn't constant!
fd1.v1 = new Value(); // OK -- not final
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
//! fd1.v2 = new Value(); // Error: Can't
//! fd1.v3 = new Value(); // change handle
//! fd1.a = new int[3];

fd1.print("fd1");
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData();
fd1.print("fd1");
fd2.print("fd2");
}
} ///:~[/code]

由於i1和I2都是具有final屬性的基本資料型別,並含有編譯期的值,所以它們除了能作為編譯期的常數使用外,在任何匯入方式中也不會出現任何不同。I3是我們體驗此類常數定義時更典型的一種方式:public表示它們可在包外使用;Static強調它們只有一個;而final表明它是一個常數。注意對於含有固定初始化值(即編譯期常數)的fianl static基本資料型別,它們的名字根據規則要全部採用大寫。也要注意i5在編譯期間是未知的,所以它沒有大寫。
不能由於某樣東西的屬性是final,就認定它的值能在編譯時期知道。i4和i5向大家證明了這一點。它們在執行期間使用隨機生成的數字。例子的這一部分也向大家揭示出將final值設為static和非static之間的差異。只有當值在執行期間初始化的前提下,這種差異才會揭示出來。因為編譯期間的值被編譯器認為是相同的。這種差異可從輸出結果中看出:

fd1: i4 = 15, i5 = 9
Creating new FinalData
fd1: i4 = 15, i5 = 9
fd2: i4 = 10, i5 = 9

注意對於fd1和fd2來說,i4的值是唯一的,但i5的值不會由於建立了另一個FinalData物件而發生改變。那是因為它的屬性是static,而且在載入時初始化,而非每建立一個物件時初始化。
從v1到v4的變數向我們揭示出final控制代碼的含義。正如大家在main()中看到的那樣,並不能認為由於v2屬於final,所以就不能再改變它的值。然而,我們確實不能再將v2繫結到一個新物件,因為它的屬性是final。這便是final對於一個控制代碼的確切含義。我們會發現同樣的含義亦適用於陣列,後者只不過是另一種型別的控制代碼而已。將控制代碼變成final看起來似乎不如將基本資料型別變成final那麼有用。

2. 空白final
Java 1.1允許我們建立“空白final”,它們屬於一些特殊的欄位。儘管被宣告成final,但卻未得到一個初始值。無論在哪種情況下,空白final都必須在實際使用前得到正確的初始化。而且編譯器會主動保證這一規定得以貫徹。然而,對於final關鍵字的各種應用,空白final具有最大的靈活性。舉個例子來說,位於類內部的一個final欄位現在對每個物件都可以有所不同,同時依然保持其“不變”的本質。下面列出一個例子:

[code]//: BlankFinal.java
// "Blank" final data members

class Poppet { }

class BlankFinal {
final int i = 0; // Initialized final
final int j; // Blank final
final Poppet p; // Blank final handle
// Blank finals MUST be initialized
// in the constructor:
BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet();
}
BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet();
}
public static void main(String[] args) {
BlankFinal bf = new BlankFinal();
}
} ///:~[/code]

現在強行要求我們對final進行賦值處理——要麼在定義欄位時使用一個表達 式,要麼在每個構建器中。這樣就可以確保final欄位在使用前獲得正確的初始化。

3. final自變數
Java 1.1允許我們將自變數設成final屬性,方法是在自變數列表中對它們進行適當的宣告。這意味著在一個方法的內部,我們不能改變自變數控制代碼指向的東西。如下所示:

[code]//: FinalArguments.java
// Using "final" with method arguments

class Gizmo {
public void spin() {}
}

public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
g.spin();
}
void without(Gizmo g) {
g = new Gizmo(); // OK -- g not final
g.spin();
}
// void f(final int i) { i++; } // Can't change
// You can only read from a final primitive:
int g(final int i) { return i + 1; }
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
} ///:~
[/code]
注意此時仍然能為final自變數分配一個null(空)控制代碼,同時編譯器不會捕獲它。這與我們對非final自變數採取的操作是一樣的。
方法f()和g()向我們展示出基本型別的自變數為final時會發生什麼情況:我們只能讀取自變數,不可改變它。

6.8.2 final方法
之所以要使用final方法,可能是出於對兩方面理由的考慮。第一個是為方法“上鎖”,防止任何繼承類改變它的本來含義。設計程式時,若希望一個方法的行為在繼承期間保持不變,而且不可被覆蓋或改寫,就可以採取這種做法。
採用final方法的第二個理由是程式執行的效率。將一個方法設成final後,編譯器就可以把對那個方法的所有呼叫都置入“嵌入”呼叫裡。只要編譯器發現一個final方法呼叫,就會(根據它自己的判斷)忽略為執行方法呼叫機制而採取的常規程式碼插入方法(將自變數壓入堆疊;跳至方法程式碼並執行它;跳回來;清除堆疊自變數;最後對返回值進行處理)。相反,它會用方法主體內實際程式碼的一個副本來替換方法呼叫。這樣做可避免方法呼叫時的系統開銷。當然,若方法體積太大,那麼程式也會變得雍腫,可能受到到不到嵌入程式碼所帶來的任何效能提升。因為任何提升都被花在方法內部的時間抵消了。Java編譯器能自動偵測這些情況,並頗為“明智”地決定是否嵌入一個final方法。然而,最好還是不要完全相信編譯器能正確地作出所有判斷。通常,只有在方法的程式碼量非常少,或者想明確禁止方法被覆蓋的時候,才應考慮將一個方法設為final。
類內所有private方法都自動成為final。由於我們不能訪問一個private方法,所以它絕對不會被其他方法覆蓋(若強行這樣做,編譯器會給出錯誤提示)。可為一個private方法新增final指示符,但卻不能為那個方法提供任何額外的含義。

6.8.3 final類
如果說整個類都是final(在它的定義前冠以final關鍵字),就表明自己不希望從這個類繼承,或者不允許其他任何人採取這種操作。換言之,出於這樣或那樣的原因,我們的類肯定不需要進行任何改變;或者出於安全方面的理由,我們不希望進行子類化(子類處理)。
除此以外,我們或許還考慮到執行效率的問題,並想確保涉及這個類各物件的所有行動都要儘可能地有效。如下所示:

[code]//: Jurassic.java
// Making an entire class final

class SmallBrain {}

final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}

//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'

public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
} ///:~[/code]

注意資料成員既可以是final,也可以不是,取決於我們具體選擇。應用於final的規則同樣適用於資料成員,無論類是否被定義成final。將類定義成final後,結果只是禁止進行繼承——沒有更多的限制。然而,由於它禁止了繼承,所以一個final類中的所有方法都預設為final。因為此時再也無法覆蓋它們。所以與我們將一個方法明確宣告為final一樣,編譯器此時有相同的效率選擇。
可為final類內的一個方法新增final指示符,但這樣做沒有任何意義。

6.8.4 final的注意事項
設計一個類時,往往需要考慮是否將一個方法設為final。可能會覺得使用自己的類時執行效率非常重要,沒有人想覆蓋自己的方法。這種想法在某些時候是正確的。
但要慎重作出自己的假定。通常,我們很難預測一個類以後會以什麼樣的形式再生或重複利用。常規用途的類尤其如此。若將一個方法定義成final,就可能杜絕了在其他程式設計師的專案中對自己的類進行繼承的途徑,因為我們根本沒有想到它會象那樣使用。
標準Java庫是闡述這一觀點的最好例子。其中特別常用的一個類是Vector。如果我們考慮程式碼的執行效率,就會發現只有不把任何方法設為final,才能使其發揮更大的作用。我們很容易就會想到自己應繼承和覆蓋如此有用的一個類,但它的設計者卻否定了我們的想法。但我們至少可以用兩個理由來反駁他們。首先,Stack(堆疊)是從Vector繼承來的,亦即Stack“是”一個Vector,這種說法是不確切的。其次,對於Vector許多重要的方法,如addElement()以及elementAt()等,它們都變成了synchronized(同步的)。正如在第14章要講到的那樣,這會造成顯著的效能開銷,可能會把final提供的效能改善抵銷得一乾二淨。因此,程式設計師不得不猜測到底應該在哪裡進行優化。在標準庫里居然採用瞭如此笨拙的設計,真不敢想象會在程式設計師裡引發什麼樣的情緒。
另一個值得注意的是Hashtable(散列表),它是另一個重要的標準類。該類沒有采用任何final方法。正如我們在本書其他地方提到的那樣,顯然一些類的設計人員與其他設計人員有著全然不同的素質(注意比較Hashtable極短的方法名與Vecor的方法名)。對類庫的使用者來說,這顯然是不應該如此輕易就能看出的。一個產品的設計變得不一致後,會加大使用者的工作量。這也從另一個側面強調了程式碼設計與檢查時需要很強的責任心。

6.9 初始化和類裝載
在許多傳統語言裡,程式都是作為啟動過程的一部分一次性載入的。隨後進行的是初始化,再是正式執行程式。在這些語言中,必須對初始化過程進行慎重的控制,保證static資料的初始化不會帶來麻煩。比如在一個static資料獲得初始化之前,就有另一個static資料希望它是一個有效值,那麼在C++中就會造成問題。
Java則沒有這樣的問題,因為它採用了不同的裝載方法。由於Java中的一切東西都是物件,所以許多活動變得更加簡單,這個問題便是其中的一例。正如下一章會講到的那樣,每個物件的程式碼都存在於獨立的檔案中。除非真的需要程式碼,否則那個檔案是不會載入的。通常,我們可認為除非那個類的一個物件構造完畢,否則程式碼不會真的載入。由於static方法存在一些細微的歧義,所以也能認為“類程式碼在首次使用的時候載入”。
首次使用的地方也是static初始化發生的地方。裝載的時候,所有static物件和static程式碼塊都會按照本來的順序初始化(亦即它們在類定義程式碼裡寫入的順序)。當然,static資料只會初始化一次。

6.9.1 繼承初始化
我們有必要對整個初始化過程有所認識,其中包括繼承,對這個過程中發生的事情有一個整體性的概念。請觀察下述程式碼:

[code]//: Beetle.java
// The full process of initialization.

class Insect {
int i = 9;
int j;
Insect() {
prt("i = " + i + ", j = " + j);
j = 39;
}
static int x1 =
prt("static Insect.x1 initialized");
static int prt(String s) {
System.out.println(s);
return 47;
}
}

public class Beetle extends Insect {
int k = prt("Beetle.k initialized");
Beetle() {
prt("k = " + k);
prt("j = " + j);
}
static int x2 =
prt("static Beetle.x2 initialized");
static int prt(String s) {
System.out.println(s);
return 63;
}
public static void main(String[] args) {
prt("Beetle constructor");
Beetle b = new Beetle();
}
} ///:~[/code]

該程式的輸出如下:

static Insect.x initialized
static Beetle.x initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 63
j = 39

對Beetle執行Java時,發生的第一件事情是裝載程式到外面找到那個類。在裝載過程中,裝載程式注意它有一個基礎類(即extends關鍵字要表達的意思),所以隨之將其載入。無論是否準備生成那個基礎類的一個物件,這個過程都會發生(請試著將物件的建立程式碼當作註釋標註出來,自己去證實)。
若基礎類含有另一個基礎類,則另一個基礎類隨即也會載入,以此類推。接下來,會在根基礎類(此時是Insect)執行static初始化,再在下一個衍生類執行,以此類推。保證這個順序是非常關鍵的,因為衍生類的初始化可能要依賴於對基礎類成員的正確初始化。
此時,必要的類已全部裝載完畢,所以能夠建立物件。首先,這個物件中的所有基本資料型別都會設成它們的預設值,而將物件控制代碼設為null。隨後會呼叫基礎類構建器。在這種情況下,呼叫是自動進行的。但也完全可以用super來自行指定構建器呼叫(就象在Beetle()構建器中的第一個操作一樣)。基礎類的構建採用與衍生類構建器完全相同的處理過程。基礎順構建器完成以後,例項變數會按本來的順序得以初始化。最後,執行構建器剩餘的主體部分。