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 会被复制到该对象内部。
如果我们在构造函数中为 final 的 foo 赋值,编译器知道构造函数只会被调用一次,因此允许在构造函数中进行赋值,这没有问题。
但如果我们尝试在方法中为 final 的 foo 赋值,编译器会拒绝,因为方法可能被多次调用,这意味着 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 中安全使用。
虽然在实际开发中你可能很少使用方法局部内部类,但为了面试准备,理解这个概念非常重要。