《Thing in Java》对final关键字的介绍之数据

来源:互联网 发布:淘宝皇冠店铺出租 编辑:程序博客网 时间:2024/06/07 15:47

final 关键字

由于语境(应用环境)不同,final 关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明
“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇
有些区别,所以也许会造成final 关键字的误用。


我们将讨论final 关键字的三种应用场合:数据、方法以及类。

final 数据

许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:
(1) 编译期常数,它永远不会改变
(2) 在运行期初始化的一个值,我们不希望它发生变化
对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期
间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型
(Primitives),而且要用 final 关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个
值。
无论static 还是 final 字段,都只能存储一个数据,而且不得改变。
若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类
型,final 会将值变成一个常数;但对于对象句柄,final 会将句柄变成一个常数。进行声明时,必须将句柄
初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java
对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有
“常数”效果)。这一限制也适用于数组,它也属于对象。

//: FinalData.java// The effect of final on fieldsclass Value {    int i = 1;}public class FinalData {    // 可以是编译器常量    final int i1 = 9;    static final int I2 = 99;    // 典型公共常数    public static final int I3 = 39;    // 无法编译器常量    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:    // 没有初始化    //数组:    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++;        // 对象不是常量        fd1.v2.i++;        // 可以 没有用final修饰        fd1.v1 = new Value();        for (int i = 0; i < fd1.a.length; i++) {            // 对象不是常量            fd1.a[i]++;        }        // 错误:不能,因为用final修饰了,永远不能将句柄变成指向另一个对象        //! fd1.v2 = new Value();        //  错误 :改变句柄        //! fd1.v3 = new Value();        //错误 :改变句柄        //! 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 = 9Creating new FinalDatafd1: i4 = 15, i5 = 9fd2: i4 = 10, i5 = 9

注意对于fd1 和 fd2 来说,i4 的值是唯一的,但 i5 的值不会由于创建了另一个FinalData 对象而发生改
变。那是因为它的属性是static,而且在载入时初始化,而非每创建一个对象时初始化。
从v1 到v4 的变量向我们揭示出final 句柄的含义。正如大家在main()中看到的那样,并不能认为由于v2
属于final,所以就不能再改变它的值。然而,我们确实不能再将v2 绑定到一个新对象,因为它的属性是
final。这便是final 对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种
类型的句柄而已。将句柄变成 final 看起来似乎不如将基本数据类型变成 final 那么有用。

  1. final 自变量
    Java 1.1 允许我们将自变量设成 final 属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一
    个方法的内部,我们不能改变自变量句柄指向的东西。如下所示:
//: FinalArguments.java// Using "final" with method argumentsclass Gizmo {    public void spin() {    }}public class FinalArguments {    void with(final Gizmo g) {        //! g = new Gizmo(); // 非法 -- g 使用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    //你只能读取用final修饰的原始值    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 时会发生什么情况:我们只能读取自变量,不可改变
它。

final 方法

之所以要使用final 方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改
变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可
以采取这种做法。
采用final 方法的第二个理由是程序执行的效率。将一个方法设成 final 后,编译器就可以把对那个方法的
所有调用都置入“嵌入”调用里。只要编译器发现一个final 方法调用,就会(根据它自己的判断)忽略为
执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除
156
堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这
样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入
代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java 编译器能自动侦测这些情
况,并颇为“明智”地决定是否嵌入一个 final 方法。然而,最好还是不要完全相信编译器能正确地作出所
有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为
final。
类内所有private 方法都自动成为final。由于我们不能访问一个 private 方法,所以它绝对不会被其他方
法覆盖(若强行这样做,编译器会给出错误提示)。可为一个 private 方法添加final 指示符,但却不能为
那个方法提供任何额外的含义。

final 类

如果说整个类都是 final(在它的定义前冠以 final 关键字),就表明自己不希望从这个类继承,或者不允
许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出
于安全方面的理由,我们不希望进行子类化(子类处理)。
除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。
如下所示:

//: Jurassic.java// Making an entire class finalclass 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 指示符,但这样做没有任何意义。

final 的注意事项

设计一个类时,往往需要考虑是否将一个方法设为 final。可能会觉得使用自己的类时执行效率非常重要,
没有人想覆盖自己的方法。这种想法在某些时候是正确的。
但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的
类尤其如此。若将一个方法定义成 final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途
径,因为我们根本没有想到它会象那样使用。
标准Java 库是阐述这一观点的最好例子。其中特别常用的一个类是 Vector。如果我们考虑代码的执行效
率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和
覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首
先,Stack(堆栈)是从Vector 继承来的,亦即Stack“是”一个 Vector,这种说法是不确切的。其次,对
于Vector 许多重要的方法,如addElement()以及 elementAt()等,它们都变成了 synchronized(同步
的)。正如在第14 章要讲到的那样,这会造成显著的性能开销,可能会把final 提供的性能改善抵销得一干
二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢
想象会在程序员里引发什么样的情绪。
另一个值得注意的是Hashtable(散列表),它是另一个重要的标准类。该类没有采用任何final 方法。正
如我们在本书其他地方提到的那样,显然一些类的设计人员与其他设计人员有着全然不同的素质(注意比较
Hashtable 极短的方法名与Vecor 的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。
一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个侧面强调了代码设计与检查时需要很强
的责任心。

原创粉丝点击