final 关键字

本文详细探讨了Java中的final关键字,包括其在数据、方法和类中的应用,以及空白final和final方法的使用规则。着重分析了final关键字如何影响代码的执行效率和类的继承关系,以及初始化和类装载的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

final 关键字
由于语境(应用环境)不同,final 关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明
“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇
有些区别,所以也许会造成final 关键字的误用。
在接下去的小节里,我们将讨论final 关键字的三种应用场合:数据、方法以及类。
6.8.1 final 数据
许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:
(1) 编译期常数,它永远不会改变
(2) 在运行期初始化的一个值,我们不希望它发生变化
对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型
(Primitives),而且要用 final 关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个
153
值。
无论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");
154
 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
155
 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 fi nal 类
如果说整个类都是 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。可能会觉得使用自己的类时执行效率非常重要,没有人想覆盖自己的方法。这种想法在某些时候是正确的。
但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的
157
类尤其如此。若将一个方法定义成 final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途
径,因为我们根本没有想到它会象那样使用。
标准Java 库是阐述这一观点的最好例子。其中特别常用的一个类是 Vector。如果我们考虑代码的执行效
率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首先,Stack(堆栈)是从Vector 继承来的,亦即Stack“是”一个 Vector,这种说法是不确切的。其次,对于Vector 许多重要的方法,如addElement()以及 elementAt()等,它们都变成了 synchronized(同步
的)。正如在第14 章要讲到的那样,这会造成显著的性能开销,可能会把final 提供的性能改善抵销得一干二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢想象会在程序员里引发什么样的情绪。
另一个值得注意的是Hashtable(散列表),它是另一个重要的标准类。该类没有采用任何final 方法。正如我们在本书其他地方提到的那样,显然一些类的设计人员与其他设计人员有着全然不同的素质(注意比较Hashtable 极短的方法名与Vecor 的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个侧面强调了代码设计与检查时需要很强的责任心。
6.9 初始化和类装载
在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程序。在这些语言中,必须对初始化过程进行慎重的控制,保证 static 数据的初始化不会带来麻烦。比如在一个static 数据获得初始化之前,就有另一个 static 数据希望它是一个有效值,那么在 C++中就会造成问
题。
Java 则没有这样的问题,因为它采用了不同的装载方法。由于 Java 中的一切东西都是对象,所以许多活动变得更加简单,这个问题便是其中的一例。正如下一章会讲到的那样,每个对象的代码都存在于独立的文件中。除非真的需要代码,否则那个文件是不会载入的。通常,我们可认为除非那个类的一个对象构造完毕,否则代码不会真的载入。由于 static 方法存在一些细微的歧义,所以也能认为“类代码在首次使用的时候载入”。
首次使用的地方也是static 初始化发生的地方。装载的时候,所有static 对象和 static 代码块都会按照本来的顺序初始化(亦即它们在类定义代码里写入的顺序)。当然,static 数据只会初始化一次。
6.9.1 继承初始化
我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。请观察下述代码:
//: Beetle.java
// The full process of initialization.
class Insect {
 int i = 9;
 int j;
 Insect() {
 prt("i = " + i + ", j = " + j);
 j = 39;
 }
 static int x1 = 
 prt("static Insect.x1 initialized");
 static int prt(String s) {
 System.out.println(s);
 return 47;
 }
}
public class Beetle extends Insect {
 int k = prt("Beetle.k initialized");
 Beetle() {
 prt("k = " + k);
 prt("j = " + j);
 }
 static int x2 =
 prt("static Beetle.x2 initialized");
 static int prt(String s) {
 System.out.println(s);
 return 63;
 }
 public static void main(String[] args) {
 prt("Beetle constructor");
 Beetle b = new Beetle();
 }
} ///:~
该程序的输出如下:
static Insect.x initialized
static Beetle.x initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 63
j = 39
对Beetle 运行Java 时,发生的第一件事情是装载程序到外面找到那个类。在装载过程中,装载程序注意它有一个基础类(即 extends 关键字要表达的意思),所以随之将其载入。无论是否准备生成那个基础类的一个对象,这个过程都会发生(请试着将对象的创建代码当作注释标注出来,自己去证实)。
若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是
Insect)执行 static 初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。
此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为 null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super 来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。基础类的构建采用与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最
后,执行构建器剩余的主体部分。

这段文字主要讨论了final关键字在Java中的应用,包括final数据、final方法和final类的使用场景以及注意事项。我会逐段解释:

1. **final数据**:
   - final关键字用于声明数据,表示该数据不能再被改变。在Java中,final数据可以是编译期常数(在编译时就确定的常量)或者在运行时初始化后不再改变的值。
   - 对于基本数据类型,final使其值成为常数;对于对象引用,则使引用指向的对象成为常数,但对象本身可以修改。

2. **空白final**:
   - Java 1.1引入了空白final的概念,即声明为final但未赋初始值的字段。空白final在构造器中必须进行初始化,并且编译器会强制执行初始化要求。

3. **final方法**:
   - final方法是指被声明为final的方法,它不能被子类覆盖或修改,保持其原有行为不变。final方法可以提高执行效率,因为编译器可以将对final方法的调用直接嵌入到调用位置,而不是使用方法调用机制。

4. **final类**:
   - final类是指被声明为final的类,它不能被继承或子类化。final类适用于不需要被修改或继承的情况,或者出于安全性考虑不允许子类化的情况。

5. **注意事项**:
   - 设计类时,需要权衡是否将方法声明为final。虽然final方法可以提高执行效率并确保行为不被修改,但也限制了类的扩展性和灵活性。
   - 标准Java库中的设计选择(如Vector类)可能会让人感到困惑,因为它们不是所有方法都声明为final,这导致了一些性能问题和设计上的不一致性。

6. **初始化和类装载**:
   - Java中的类装载和初始化是延迟加载的,只有在需要使用类或对象时才会加载和初始化。静态成员的初始化顺序是按照类定义中的顺序进行的,且只初始化一次。

7. **继承初始化**:
   - 继承中,基类会先进行静态初始化,然后依次是派生类。对象的初始化包括对基本数据类型的默认值和对象引用的null值的赋值,然后调用基类的构造器,依次初始化实例变量,并执行构造器主体。

这段文字详细介绍了final关键字在Java中的各种应用场景和注意事项,以及类的加载和初始化过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值