图 1:本文封面,三分支语义树总览。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「final 有什么用」几乎每个 Java 面试都会问,多数人能背出三个位置的答案,但面试官追问「final 引用指向的对象能改吗」「final 在并发里还有什么用」时,背书就露馅了。这道题真正考的是:final 锁住的到底是哪条边界。
三个位置,三种「不许」。
Java 语言规范把 final 的语义分散在三处。修饰类(JLS 8.1.1.2):这个类不能有子类,继承的大门被焊死,String、Math 这类基础与工具类都是 final 的——设计者不希望任何人通过继承改写它们的行为契约。修饰方法(JLS 8.4.3.3):子类可以继承并调用这个方法,但不能覆写它,实现被钉死。修饰变量(JLS 8.3.1.2):只能被赋值一次。对基本类型,「赋值一次」就是值从此不变;对引用类型,是引用的指向从此不变——这两者的差别正是第二个考点。
引用不可变,不等于对象不可变。
这是最容易答错的一问。final 修饰引用变量时,锁住的是「这个变量指向谁」,不是「指向的那个对象长什么样」。一个 final 的 List,你不能再让它指向另一个 List,但往这个 List 里增删元素完全合法。真正的不可变对象(比如 String)靠的是一整套设计手段——类 final、字段私有 final、不提供任何修改方法,final 关键字只是其中一块砖。别把关系说反:final 引用是造不可变对象的必要材料之一,但光有 final 引用,对象照样可以是可变的。
用代码把语义跑出来。
下面这段程序演示三个位置的行为(本机 JDK 21 实测,javac FinalDemo.java && java FinalDemo):
import java.util.ArrayList;
import java.util.List;
public class FinalDemo {
// final 类不能被继承:String 就是典型代表,取消下面这行注释会编译报错
// static class MyString extends String {}
static class Base {
final void connect() { System.out.println("Base.connect 执行"); }
}
static class Child extends Base {
// final 方法不能覆写:取消注释同样编译报错
// @Override void connect() {}
}
public static void main(String[] args) {
final int limit = 100;
// limit = 200; // 编译错误:final 变量不能二次赋值
System.out.println("final 基本类型变量: limit = " + limit);
final List<String> config = new ArrayList<>();
config.add("model=A"); // 引用是 final,对象内容照样能改
config.add("retry=3");
// config = new ArrayList<>(); // 编译错误:final 引用不能改指向
System.out.println("final 引用没换,但列表内容变了: " + config);
new Child().connect(); // 子类继承并使用 final 方法,只是不能改实现
}
}
实测输出:
final 基本类型变量: limit = 100
final 引用没换,但列表内容变了: [model=A, retry=3]
Base.connect 执行
被注释掉的四行如果放出来,编译器会直接拒绝。比如给 final 变量二次赋值,javac 实测报错:
FinalError.java:4: error: cannot assign a value to final variable limit
最后一行输出值得多看一眼:子类 Child 继承并正常调用了 final 方法 connect——final 方法禁的是覆写,不是使用。
图 2:三重语义、引用与对象的分野、并发保证。
并发里的 final:安全发布的底线。
final 最深的一层语义藏在内存模型里。JLS 17.5 节给出一条重要保证:一个对象如果在构造器正常结束后才被其他线程看到,那么这些线程不需要任何额外同步,就一定能读到它 final 字段的正确初始化值。规范原文的例子对比很直观:同一个对象里,final 字段保证看到构造时写入的值,而普通字段可能还是默认值零。这就是「安全发布」:不可变对象可以不加锁地在线程之间传递,靠的就是 final 字段的这条语义。
但这条保证有个前提,规范同样写明了:构造期间不能让 this 引用逸出——比如在构造器里把 this 注册进某个全局容器,别的线程就可能在构造器跑完之前看到「半成品」对象,final 的保护随之失效。所以并发任务里到处传递的配置对象用 final 钉死字段,不只是代码风格问题,而是在买这条内存模型保证。
回到面试现场,建议的答题结构:先按三个位置给出「不能继承、不能覆写、只能赋值一次」的基本盘,再主动拆「引用不变与对象不变」这个陷阱,最后把安全发布与 this 逸出前提抛出来。基本盘是及格线,并发语义是区分度。你在代码里给哪些对象上过 final,是图不可变,还是图这条并发保证?欢迎聊聊你的用法。