Java final 关键字详解

更新于 2025-12-27

baeldung 2018-03-27

1. 概述

虽然继承使我们能够重用现有代码,但有时出于各种原因,我们需要对可扩展性施加限制。final 关键字正好可以实现这一目的。

在本教程中,我们将探讨 final 关键字对类、方法和变量分别意味着什么。

2. final 类

被标记为 final 的类不能被继承。如果我们查看 Java 核心库的源码,会发现其中包含许多 final 类,例如 String 类。

试想一下,如果我们可以继承 String 类、重写其任意方法,并将所有 String 实例替换为我们自定义的 String 子类实例,那么对字符串对象的操作结果将变得不可预测。而由于 String 类在 Java 中无处不在,这种不确定性是完全不可接受的。因此,String 类被声明为 final

任何试图继承 final 类的行为都会导致编译错误。为了演示这一点,我们先创建一个 finalCat

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,但它在某些场景下是一种优秀的设计选择,有助于提升代码的安全性、可读性和可维护性。