許多程序設計語言都有自己的辦法告訴編譯器某個數據是“常數”。常數主要應用於下述兩個方面:
(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時會發生什麼情況:我們只能讀取自變量,不可改變它。