Java 中的 final 关键字是如何工作的?

更新于 2025-12-27

AmitG 2024-10-11

Java 中的 final 关键字是如何工作的?我仍然可以修改对象。

这是一个面试中的经典问题。通过这个问题,面试官试图考察你对对象在构造函数、方法、类变量(静态变量)和实例变量方面的行为理解程度。

如今,面试官还经常问另一个热门问题:Java 1.8 中的“effectively final”(有效 final)是什么?
我会在文章最后解释 Java 1.8 中的这个概念。

import java.util.ArrayList;
import java.util.List;

class Test {
    private final List foo; // 注释-1    
    public Test() {
        foo = new ArrayList(); // 注释-2
        foo.add("foo"); // 修改-1   注释-3
    }

    public void setFoo(List foo) {
       //this.foo = foo; 会导致编译错误。
    }
}

在上述代码中,我们为 Test 类定义了一个构造函数,并提供了一个 setFoo 方法。


关于构造函数

构造函数在每次使用 new 关键字创建对象时只能被调用一次。你不能多次调用构造函数,因为它的设计初衷就是如此。

关于方法

方法可以被调用任意多次(甚至一次都不调用),编译器清楚这一点。


场景 1

private final List foo;  // 1

foo 是一个实例变量。当我们创建 Test 类的对象时,实例变量 foo 会被复制到该对象内部。

如果我们在构造函数中finalfoo 赋值,编译器知道构造函数只会被调用一次,因此允许在构造函数中进行赋值,这没有问题。

但如果我们尝试在方法中finalfoo 赋值,编译器会拒绝,因为方法可能被多次调用,这意味着 final 变量的值可能会被多次更改——而这正是 final 所禁止的。因此,编译器认为构造函数是初始化 final 实例变量的合适位置。你只能一次性final 变量赋值。


场景 2

private static final List foo = new ArrayList();

现在 foo 是一个静态变量。当我们创建 Test 类的实例时,foo 不会被复制到每个对象中,因为它是 static 的。这意味着 foo 不再是每个对象的独立属性,而是属于 Test 类本身的共享属性。

如果我们在构造函数中初始化 static final 变量,那么每次创建新对象(即每次调用构造函数)都会试图修改这个共享的 final static 变量——这是不允许的。

因此,编译器规定:static final 变量不能在构造函数中初始化,也不能通过方法赋值。你必须在声明的同时完成初始化(如上面注释-1 所示的位置)。


场景 3

t.foo.add("bar"); // 修改-2

这正是你问题中提到的情况。在这里,你并没有改变 foo 引用所指向的对象,而只是向该对象(ArrayList)中添加了内容,这是完全允许的。

编译器只会在你试图将 foo 重新指向另一个新对象(例如 foo = new ArrayList();)时报错。

规则:如果你已经初始化了一个 final 变量,那么你就不能再让它引用另一个不同的对象。(在这个例子中,就是 ArrayList 对象)


其他关于 final 的要点

  • final 类不能被继承(即不能有子类)。
  • final 方法不能被重写(当该方法在父类中时)。
  • 子类中的方法可以是 final(这句话要从语法角度理解:子类可以定义自己的 final 方法,也可以重写父类非 final 方法并将其标记为 final)。

Java 1.8 中的 “Effectively Final”(有效 final)是什么?

public class EffectivelyFinalDemo { // 使用 Java 1.8 编译
    public void process() {
        int thisValueIsFinalWithoutFinalKeyword = 10; // 这个变量是“有效 final”的
        
        // 要使其成为 effectively final,就不能重新赋值。请删除下面这一行(按提示操作)
        thisValueIsFinalWithoutFinalKeyword = getNewValue(); // 删除此行以测试 effectively final
        
        class MethodLocalClass {
            public void innerMethod() {
                // 下面这行会报编译错误:
                // "Local variable thisValueIsFinalWithoutFinalKeyword defined in an enclosing scope must be final or effectively final"
                System.out.println(thisValueIsFinalWithoutFinalKeyword); 
                // 在方法局部内部类中,只能访问 final 或 effectively final 的局部变量
                // 如果你想测试 effectively final 是否生效,请删除上面提到的赋值语句。
            }
        }
    }

    private int getNewValue() {
        return 0;
    }
}

在 Java 1.7 及更早版本中,如果你要在方法局部内部类(method local inner class)或Lambda 表达式中访问局部变量,必须显式地用 final 修饰该变量

但从 Java 1.8 开始,只要一个局部变量在初始化后从未被重新赋值,即使没有显式加上 final 关键字,它也被视为 “effectively final”,可以在内部类或 Lambda 中安全使用。

虽然在实际开发中你可能很少使用方法局部内部类,但为了面试准备,理解这个概念非常重要。