baeldung 2018-03-27
1. 概述
虽然继承使我们能够重用现有代码,但有时出于各种原因,我们需要对可扩展性施加限制。final 关键字正好可以实现这一目的。
在本教程中,我们将探讨 final 关键字对类、方法和变量分别意味着什么。
2. final 类
被标记为 final 的类不能被继承。如果我们查看 Java 核心库的源码,会发现其中包含许多 final 类,例如 String 类。
试想一下,如果我们可以继承 String 类、重写其任意方法,并将所有 String 实例替换为我们自定义的 String 子类实例,那么对字符串对象的操作结果将变得不可预测。而由于 String 类在 Java 中无处不在,这种不确定性是完全不可接受的。因此,String 类被声明为 final。
任何试图继承 final 类的行为都会导致编译错误。为了演示这一点,我们先创建一个 final 类 Cat:
public final class Cat {
private int weight;
// 标准的 getter 和 setter 方法
}
然后尝试继承它:
public class BlackCat extends Cat {
}
此时编译器会报错:
The type BlackCat cannot subclass the final class Cat
注意:在类声明中使用
final关键字并不意味着该类的对象是不可变的(immutable)。我们可以自由地修改Cat对象的字段:
Cat cat = new Cat();
cat.setWeight(1);
assertEquals(1, cat.getWeight());
我们只是无法继承它而已。
从良好的设计原则出发,我们应该仔细设计并文档化一个类,或者出于安全考虑将其声明为 final。然而,在创建 final 类时也需谨慎。
因为一旦一个类被声明为 final,其他程序员就无法通过继承来改进它。假设我们正在使用某个类,但没有它的源代码,而其中某个方法存在问题。如果该类是 final 的,我们就无法通过继承来重写该方法以修复问题。换句话说,我们会失去面向对象编程的一大优势——可扩展性。
3. final 方法
被标记为 final 的方法不能被重写(override)。当我们设计一个类,并认为某个方法不应该被子类覆盖时,就可以将其声明为 final。Java 核心库中也包含大量 final 方法。
有时候,我们并不希望完全禁止类的继承,而只是希望防止某些特定方法被覆盖。一个典型的例子是 Thread 类:我们可以合法地继承它以创建自定义线程类,但它的 isAlive() 方法是 final 的。
isAlive() 方法用于检查线程是否处于活动状态。由于该方法是 native(本地方法),其实现在操作系统和硬件相关的底层代码中,因此几乎不可能被正确重写。
让我们创建一个 Dog 类,并将其 sound() 方法声明为 final:
public class Dog {
public final void sound() {
// ...
}
}
然后尝试继承 Dog 并重写 sound() 方法:
public class BlackDog extends Dog {
public void sound() {
}
}
此时编译器会报错:
- overrides com.msm.finalkeyword.Dog.sound
- Cannot override the final method from Dog
sound() method is final and can’t be overridden
建议:如果类中的某些方法会被其他方法调用,应考虑将这些被调用的方法声明为
final。否则,子类重写这些方法可能会影响调用者的行为,导致意外结果。
特别地,如果构造函数中调用了其他方法,通常应将这些方法声明为final。
思考:将类中所有方法都声明为 final 与直接将整个类声明为 final 有何区别?
- 前者允许继承该类并添加新方法;
- 后者则完全禁止继承。
4. final 变量
被标记为 final 的变量不能被重新赋值。一旦 final 变量被初始化,其值就不可更改。
4.1. final 基本类型变量
声明一个基本类型的 final 变量 i,并赋值为 1:
public void whenFinalVariableAssign_thenOnlyOnce() {
final int i = 1;
// ...
i = 2; // 尝试重新赋值
}
编译器会报错:
The final local variable i may already have been assigned
4.2. final 引用变量
对于 final 引用变量,同样不能重新赋值。但这并不意味着它所指向的对象是不可变的。我们仍然可以自由修改该对象的属性。
例如:
final Cat cat = new Cat();
如果尝试重新赋值:
cat = new Cat(); // 编译错误
编译器会提示:
The final local variable cat cannot be assigned. It must be blank and not using a compound assignment
但我们仍可修改 Cat 实例的属性:
cat.setWeight(5);
assertEquals(5, cat.getWeight());
4.3. final 字段(成员变量)
final 字段可以是常量,也可以是只写一次的字段。如何区分?可以问自己一个问题:如果序列化这个对象,我们会包含这个字段吗?
- 如果不会,那它就是一个常量;
- 如果会,那它是对象状态的一部分。
根据命名规范,类常量应全部大写,单词间用下划线 _ 分隔:
static final int MAX_WIDTH = 999;
重要规则:所有
final字段必须在构造函数执行完成前完成初始化。
静态
final字段可在以下位置初始化:- 声明时(如上例)
- 静态初始化块(static initializer block)中
实例
final字段可在以下位置初始化:- 声明时
- 实例初始化块(instance initializer block)中
- 构造函数中
否则,编译器将报错。
4.4. final 参数
方法参数也可以使用 final 修饰。final 参数在方法体内不能被修改:
public void methodWithFinalArguments(final int x) {
x = 1; // 编译错误
}
编译器会提示:
The final local variable x cannot be assigned. It must be blank and not using a compound assignment
5. 结论
在本文中,我们学习了 final 关键字在类、方法和变量上的不同含义。尽管我们在内部代码中可能不常使用 final,但它在某些场景下是一种优秀的设计选择,有助于提升代码的安全性、可读性和可维护性。