按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
…………………………………………………………Page 155……………………………………………………………
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
154
…………………………………………………………Page 156……………………………………………………………
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 方法调用,就会(根据它自己的判断)忽略为
执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除
155
…………………………………………………………Page 157……………………………………………………………
堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这
样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入
代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。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。可能会觉得使用自己的类时执行效率非常重要,
没有人想覆盖自己的方法。这种想法在某些时候是正确的。
但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的
156
…………………………………………………………Page 158……………………………………………………………
类尤其如此。若将一个方法定义成 final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途
径,因为我们根本没有想到它会象那样使用。
标准Java 库是阐述这一观点的最好例子。其中特别常用的一个类是 Vector。如果我们考虑代码的执行效
率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和
覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首
先,Stack (堆栈)是从Vector 继承来的,亦即Stack “是”一个Vector,这种说法是不确切的。其次,对
于Vector 许多重要的方法,如addElement()以及elementAt()等,它们都变成了 synchronized (同步
的)。正如在第14章要讲到的那样,这会造成显著的性能开销,可能会把final 提供的性能改善抵销得一干
二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙�