# 6.8 `final`關鍵字
由于語境(應用環境)不同,`final`關鍵字的含義可能會稍微產生一些差異。但它最一般的意思就是聲明“這個東西不能改變”。之所以要禁止改變,可能是考慮到兩方面的因素:設計或效率。由于這兩個原因頗有些區別,所以也許會造成`final`關鍵字的誤用。
在接下去的小節里,我們將討論`final`關鍵字的三種應用場合:數據、方法以及類。
## 6.8.1 `final`數據
許多程序設計語言都有自己的辦法告訴編譯器某個數據是“常數”。常數主要應用于下述兩個方面:
(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`時會發生什么情況:我們只能讀取參數,不可改變它。
## 6.8.2 `final`方法
之所以要使用`final`方法,可能是出于對兩方面理由的考慮。第一個是為方法“上鎖”,防止任何繼承類改變它的本來含義。設計程序時,若希望一個方法的行為在繼承期間保持不變,而且不可被覆蓋或改寫,就可以采取這種做法。
采用`final`方法的第二個理由是程序執行的效率。將一個方法設成`final`后,編譯器就可以把對那個方法的所有調用都置入“嵌入”調用里。只要編譯器發現一個`final`方法調用,就會(根據它自己的判斷)忽略為執行方法調用機制而采取的常規代碼插入方法(將參數壓入棧;跳至方法代碼并執行它;跳回來;清除棧參數;最后對返回值進行處理)。相反,它會用方法主體內實際代碼的一個副本來替換方法調用。這樣做可避免方法調用時的系統開銷。當然,若方法體積太大,那么程序也會變得雍腫,可能受到到不到嵌入代碼所帶來的任何性能提升。因為任何提升都被花在方法內部的時間抵消了。Java編譯器能自動偵測這些情況,并頗為“明智”地決定是否嵌入一個`final`方法。然而,最好還是不要完全相信編譯器能正確地作出所有判斷。通常,只有在方法的代碼量非常少,或者想明確禁止方法被覆蓋的時候,才應考慮將一個方法設為`final`。
類內所有`private`方法都自動成為`final`。由于我們不能訪問一個`private`方法,所以它絕對不會被其他方法覆蓋(若強行這樣做,編譯器會給出錯誤提示)。可為一個`private`方法添加`final`指示符,但卻不能為那個方法提供任何額外的含義。
## 6.8.3 `final`類
如果說整個類都是`final`(在它的定義前冠以`final`關鍵字),就表明自己不希望從這個類繼承,或者不允許其他任何人采取這種操作。換言之,出于這樣或那樣的原因,我們的類肯定不需要進行任何改變;或者出于安全方面的理由,我們不希望進行子類化(子類處理)。
除此以外,我們或許還考慮到執行效率的問題,并想確保涉及這個類各對象的所有行動都要盡可能地有效。如下所示:
```
//: 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++;
}
} ///:~
```
注意數據成員既可以是`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`極短的方法名與`Vector`的方法名)。對類庫的用戶來說,這顯然是不應該如此輕易就能看出的。一個產品的設計變得不一致后,會加大用戶的工作量。這也從另一個側面強調了代碼設計與檢查時需要很強的責任心。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物