6.8 final 關鍵字 和 6 . 8 . 1 f i n a l 資料
由於語境(應用環境)不同,final 關鍵字的含義可能會稍微產生一些差異。但它最一般的意思就是宣告
“這個東西不能改變”。之所以要禁止改變,可能是考慮到兩方面的因素:設計或效率。由於這兩個原因頗
有些區別,所以也許會造成final 關鍵字的誤用。
在接下去的小節裡,我們將討論final 關鍵字的三種應用場合:資料、方法以及類。
6 . 8 . 1 f i n a l 資料許多程式設計語言都有自己的辦法告訴編譯器某個資料是“常數”。常數主要應用於下述兩個方面:
(1) 編譯期常數,它永遠不會改變
(2) 在執行期初始化的一個值,我們不希望它發生變化
對於編譯期的常數,編譯器(程式)可將常數值“封裝”到需要的計算過程裡。也就是說,計算可在編譯期
間提前執行,從而節省執行時的一些開銷。在Java 中,這些形式的常數必須屬於基本資料型別
(Primitives),而且要用final 關鍵字進行表達。在對這樣的一個常數進行定義的時候,必須給出一個
值。
無論static 還是final 欄位,都只能儲存一個數據,而且不得改變。
若隨同物件控制代碼使用final,而不是基本資料型別,它的含義就稍微讓人有點兒迷糊了。對於基本資料類
型,final 會將值變成一個常數;但對於物件控制代碼,final 會將控制代碼變成一個常數。進行宣告時,必須將控制代碼
初始化到一個具體的物件。而且永遠不能將控制代碼變成指向另一個物件。然而,物件本身是可以修改的。Java
對此未提供任何手段,可將一個物件直接變成一個常數(但是,我們可自己編寫一個類,使其中的物件具有
“常數”效果)。這一限制也適用於陣列,它也屬於物件。
下面是演示final 欄位用法的一個例子:
//: 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");
}
} ///:~
由於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 欄位現在對每個物件都可以有所不同,同時依然保持其“不變”的本質。下面列出一
個例子:
//: 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();
}
} ///:~
現在強行要求我們對final 進行賦值處理——要麼在定義欄位時使用一個表達 式,要麼在每個構建器中。這
樣就可以確保final 欄位在使用前獲得正確的初始化。
3. final 自變數
Java 1.1 允許我們將自變數設成final 屬性,方法是在自變數列表中對它們進行適當的宣告。這意味著在一
個方法的內部,我們不能改變自變數控制代碼指向的東西。如下所示:
//: 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);
}
} ///:~
注意此時仍然能為final 自變數分配一個null(空)控制代碼,同時編譯器不會捕獲它。這與我們對非final 自
變數採取的操作是一樣的。
方法f()和g()向我們展示出基本型別的自變數為final 時會發生什麼情況:我們只能讀取自變數,不可改變
它。