final 关键字有什么作用?final、finally、finalize 有什么区别?
简化版
final 表示「不可变」,修饰变量(值/引用不能再改)、方法(不能被重写)、类(不能被继承)。而 final、finally、finalize 是三个八竿子打不着的东西:final 是修饰符,finally 是异常处理里「总会执行」的代码块,finalize 是对象被回收前调用的方法(已废弃)。名字像,作用毫无关系。
详细版
final 的三种用法:
- 修饰变量:变量一旦赋值就不能再改。基本类型是值不能变;引用类型是引用不能变(但对象内容可以变)。
final int x = 1; // x 不能再赋值
final List<String> list = new ArrayList<>();
list.add("a"); // ✅ 对象内容能改
list = new ArrayList<>(); // ❌ 编译错误,引用不能改指向
- 修饰方法:该方法不能被子类重写(override),常用于锁定关键逻辑。
- 修饰类:该类不能被继承,如
String、Integer都是final类,防止被破坏行为。
final / finally / finalize 的区别:
| 名称 | 是什么 | 作用 |
|---|---|---|
final | 修饰符 | 修饰变量/方法/类,表示不可变/不可重写/不可继承 |
finally | 异常处理代码块 | try-catch-finally 里无论是否异常都会执行,用于释放资源 |
finalize | Object 的方法 | 对象被 GC 回收前调用,已废弃(Java 9 起 deprecated),别用 |
完整版教学
一、final 修饰引用类型:最容易踩的坑
初学者常误以为「final 的对象内容也不能改」,其实 final 锁的是引用(指向哪个对象),不是对象的内容:
final StringBuilder sb = new StringBuilder("a");
sb.append("b"); // ✅ 内容变成 "ab",合法
sb = new StringBuilder(); // ❌ 想让 sb 指向新对象,编译报错
想让对象内容也不可变,得靠对象本身设计成不可变(像 String),final 帮不了这个忙。final 只保证「引用不变」,不保证「对象不变」。
二、final 的三大好处
- 不可变、线程安全:
final字段一旦构造完成就不再变,天然线程安全,多线程读无需同步。这也是为什么不可变对象(String、包装类)适合并发共享。 - 语义清晰、防误改:把「不该变的东西」标成
final,编译器帮你拦住意外修改,代码意图也更明确(比如常量public static final)。 - 配合 JMM 的可见性保证:正确构造的
final字段,在对象发布后对其他线程可见,无需额外同步(这是 JMM 对 final 的特殊保证)。
补充:局部变量被 lambda 或匿名内部类捕获时,必须是
final或「事实上 final(effectively final,赋值后没再改)」——因为捕获的是值的副本,不允许它变来变去。
三、finally 的执行规则与陷阱
finally 的核心承诺是无论 try 里是否抛异常、是否 return,它都会执行,所以用来释放资源(关连接、关文件):
try {
return 1;
} finally {
System.out.println("总会执行"); // 即使上面 return 了也会先执行这里
}
两个陷阱:
- 别在 finally 里 return 或抛异常:会覆盖 try 里的返回值/异常,导致真正的结果/异常丢失。
try { return 1; }
finally { return 2; } // ❌ 实际返回 2,try 的 1 被吃掉了
- 资源释放优先用 try-with-resources:它自动关闭
AutoCloseable资源,比手写 finally 更安全(详见「受检异常和运行时异常的区别」那道题)。
四、finalize 为什么被废弃
finalize() 本意是「对象被回收前给它一次清理机会」,但它问题太多:调用时机不确定(可能永远不执行)、拖慢 GC、可能让对象「复活」、异常被吞。所以 Java 9 起标记为废弃,官方推荐用 try-with-resources 或 Cleaner 替代。面试记住结论:finalize 不要用。
五、编译期常量与 final 字段的发布语义
并非所有 final 变量都是编译期常量。final int A = 10 和 static final String NAME = "api" 可以在编译期直接内联到调用方字节码;而 final int B = random.nextInt() 只保证赋值一次,值仍要到运行期才能确定。正因为可能被内联,修改公共常量后只重新编译提供方,旧调用方仍可能保留旧值,所以公共协议常量变更要整体重新构建。
static final int TIMEOUT_SECONDS = 30; // 编译期常量,可被内联
static final Integer LIMIT = 100; // 包装对象,不是常量表达式
final UUID requestId = UUID.randomUUID(); // 运行期赋值一次
JMM 还给正确构造的 final 字段提供特殊可见性:构造器结束前正常写入的 final 值,在对象没有从构造器中逸出的前提下,被其他线程看到时不会表现成默认值。它解决的是初始化安全,不代表引用指向的可变集合自动线程安全。
| 说法 | 是否正确 | 边界 |
|---|---|---|
| final 基本类型不能重新赋值 | 是 | 可在声明处或构造路径中赋值一次 |
| final 引用指向的对象不能变化 | 否 | 不能换引用,但对象内部仍可变 |
| final 字段天然使整个对象线程安全 | 否 | 还要约束其他可变状态和发布方式 |
| static final 一定是编译期常量 | 否 | 初始化式必须是常量表达式且类型适合 |
六、常见误区与追问
- 误区:认为
final List的元素不能修改。 final 只锁定变量保存的引用,列表仍可add。 - 误区:把
final、finally、finalize当作一套机制。 它们分别属于声明、异常控制流和已废弃的对象终结机制。 - 误区:在
finally中返回。 它会覆盖 try 的返回值或正在传播的异常,造成排障困难。 - 追问:final 方法能否重载? 可以;它禁止子类重写同一签名,不妨碍同类声明不同参数列表的方法。
- 追问:finally 是否绝对执行? 进程被强制终止、JVM 崩溃等情况下不能保证,正常控制流下也应优先使用 try-with-resources 管理资源。
- 追问:final 字段是否等于不可变对象? 不是;不可变对象还要求不暴露可变内部状态、没有修改入口,并通常禁止可破坏语义的继承。
记忆钩子:final 管的是“这个名字还能不能换绑定”,对象内容能否变化要继续看对象自己的设计。
七、加强记忆
从三种修饰目标串联 final:变量只能完成一次绑定,方法锁定继承扩展点,类关闭继承入口;对引用变量尤其要分清“引用不变”与“对象不变”。finally 是异常控制流中的清理保障,但不应 return;finalize() 则因不确定、慢和可复活等问题被淘汰。再进一层,编译期常量可能被内联,final 字段拥有初始化安全语义,这些都不等于整个对象自动不可变或线程安全。