# 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 推荐读物