当前位置: 首页 > news >正文

Java 中 final、finally 与 finalize():语义、底层机制与常见误区

目录

一. finalize() 详解

1.1 finalize 方法是什么?

1.2 finalize 方法的作用?

1.3 为什么强烈不建议在 finalize 方法中进行资源的释放和清理工作

1.4 版本说明

二. finally 详解

2.1 基本语义

2.2 return 遇到 finally

2.3 finally 也可能掩盖原始异常

2.4 finally 也并非绝对执行

三. final 关键字详解

3.1 修饰类

3.2 修饰方法

3.3 修饰字段

3.4 修饰基本类型变量和引用类型变量

3.5 修饰局部变量

3.6 修饰方法参数

3.7 final 不保证线程安全

3.8 static final 修饰的不一定是编译期常量


先说结论,final,finally,finalize 三者没有直接语义关联

final:编译器约束与JVM语义;

finally:异常控制与资源清理;

finalize():对象终结机制及其淘汰策略;

一. finalize() 详解

1.1 finalize 方法是什么?

首先要知道,finalize 方法是 Object 类中的一个方法,从源码中看到,所以所有的对象都会具有这个方法,该方法修饰符为 protected ,子类可以进行 @Override 重写,但实际开发过程中一般不在意;

在 JDK9 之后,该方法已经被官方不推荐使用了,但面试的时候还是有可能会被问到的,所以我们就来聊聊该方法的一些作用。

1.2 finalize 方法的作用?

finalize()是旧版 Java 提供的对象终结回调。当 JVM 发现一个重写了finalize()的对象已经不可达时,可能在回收其存储空间前调用该方法。但调用时间不确定,也不保证一定执行。即使对象不可达,也可能因为 JVM 很快退出等原因,从未执行finalize()

当我们将一个对象设置为 null 之后,它和对象的引用关系就断开了,若被对象不能再从任何 GC Roots 通过引用链到达时,它才可能被判定为不可达对象;

虽然 finalize()历史上曾被设计用于对象终结和资源清理,但实践证明,这种清理方式缺乏确定性,不应承担文件、Socket、数据库连接等资源的释放工作。

如下代码所示

public class Test { // 重写 Test 类中的 finalize() 方法 @Override public void finalize() throws Throwable{ // 打印一句话作为标记,证明该方法被调用过 System.out.println("finalize方法执行"); } public static void main(String[] args) throws Exception { // 创建类对象 t Test t = new Test(); // 获取该对象 System.out.println(t+"第一次获取对象"); // 断开局部变量 t 与对象之间的引用 t = null; // 仅向 JVM 提出执行垃圾回收的建议,不保证立即发生 GC, // 也不保证 finalize() 一定被调用。 System.gc(); // 仅为了提高观察到 finalize() 输出的概率,不提供任何执行保证 Thread.sleep(2000); // 经过GC之后再次获取t对象 System.out.println(t+"第二次获取对象"); } }

垃圾回收和对象终结由 JVM 内部机制处理,与业务线程一部运行,执行时机独立于业务线程。程序退出时,JVM 不保证等待所有待终结对象执行 finalize();

然后我们运行上述方法,在控制台中得到如下所示结果

手动调用GC,JVM 接受了System.gc()请求,并调用了该对象的finalize()。该结果只能说明本次运行观察到了终结回调,不能证明将引用赋值为null后一定会执行finalize()

1.3 为什么强烈不建议在 finalize 方法中进行资源的释放和清理工作

原因我总结了以下三点

(1)资源释放实际成谜

如果我们重写了 finalize() 方法,那么对象在变成垃圾被垃圾回收器回收之前,需要先执行 finalize() 方法,而 finalize() 方法并不是由垃圾回收器来调用的,是有一个专门的线程叫 finalizer 来调用的,finalizer 线程内部有一个引用队列,垃圾回收器识别到垃圾对象之后,会把该对象添加到引用队列中去,finalizer 线程会依次调用每个对象的 finalize() 方法然后进行清理。

但是垃圾回收时机不确定,终结队列的处理速度也不可控,因此外部资源的释放时间无法得到保证。文件描述符、Socket 和数据库连接属于稀缺资源,不能等待不确定的 GC 周期进行释放。

(2)资源释放异常

finalize()既然是一个方法,那么它就有可能抛出异常,如果finalize()抛出未捕获异常,该异常通常会被终结机制忽略,本次finalize()调用随之结束。对象不会仅仅因为该异常就永久无法回收,但预期的资源清理逻辑可能没有完整执行。

(3)很容易导致OOM(内存溢出)

GC垃圾回收线程和主程序的线程不是同一个线程。也就是说,我们的主线程,也可以说是业务一直都在持续地进行着,那么业务就很有可能会在内存中继续创建出新的对象进行其他的业务操作。而此时先前创建的对象被回收需要调用 finalize 方法,如果我们将一些资源的释放工作和内存清理工作全部放到 finalize 方法中的话,是非常耗时的,特别是网络断开连接操作,需要等待对方的响应才能断开连接,但现在内存中的垃圾对象又需要等待 finalize 方法执行完毕后才能清除,由于业务的不断进行,还有不断新的对象被创建,垃圾对象的清理速度慢,新对象的创建速度快,清理的速度没有创建的速度快,finalize()执行缓慢或发生阻塞,待终结对象可能在队列中持续积压。这些对象及其关联数据暂时无法释放,从而增加内存压力,严重时可能导致OutOfMemoryError

1.4 版本说明

finalize() 从JDK9开始标记为弃用;JDK18通过 JEP 421 将终结机制标记为"弃用并移除计划"。新代码不应再使用这一机制。

二. finally 详解

2.1 基本语义

finally表示退出trycatch前需要执行的收尾逻辑。try可以直接和finally组合,不强制要求存在catch

lock.lock(); try { process(); } finally { lock.unlock(); }

无论 process() 正常返回还是抛出异常,都应该释放锁

2.2 return 遇到 finally

static int test() { int value = 1; try { return value; } finally { value = 2; } }

结果仍然是 1,执行 return value 时,返回值已经被计算并暂存,随后才执行 finally。

但是,如果 finally 自己也执行 return:

static int test() { try { return 1; } finally { return 2; } }

结果为2。 finally 的突然完成会覆盖此前的返回或异常,因此严禁在 finally 中使用 return;

2.3 finally 也可能掩盖原始异常

try { throw new IllegalStateException("业务异常"); } finally { throw new RuntimeException("清理异常"); }

上方代码执行后,调用方看到的是"清理异常",原始"业务异常"被掩盖。

这也是 try-with-resources 更可靠的原因:如果业务代码和 close() 都抛出异常,业务异常作为主异常,关闭资源产生的异常会通过 getSuppressed() 保存。

2.4 finally 也并非绝对执行

在以下情况下,finally 不保证执行

  1. System.exit() 或 Runtime.halt();
  2. JVM或操作系统异常终止;
  3. 进程被强制杀死;
  4. 机器断电;
  5. 控制流永远无法离开 try,例如死循环或永久阻塞;

因此,finally 能保证的是 Java 控制流正常离开响应代码区域的收尾,不是进程级别的绝对保证。

三. final 关键字详解

3.1 修饰类

当 final 修饰类时,表示该类不可被继承。将类声明为final是实现不可变类的一种手段,但final类并不必然是不可变类。String的不可变性还依赖字段封装、不提供修改内部状态的方法,以及对内部数据的安全处理。因此finall 类 ≠ 不可变类

final 和 abstract 不能同时修饰一个类,因为前者禁止继承,而后者要求通过继承实现。

需要强调:final 类不等于类中的所有字段都是常量,它只是禁止继承。

3.2 修饰方法

final实例方法不能被子类重写,但仍然可以被重载。构造器不能使用final修饰,因为构造器本身不参与继承和重写。finalabstract也不能同时修饰一个方法。

class Parent { public final void execute() { } public void execute(String command) { // 这是重载,不是重写 } }

3.3 修饰字段

观察下方关于 final修饰实例字段(name,age)静态字段(MAX_SIZE)的相关代码

class Example { static final int MAX_SIZE; final String name; final int version; static { MAX_SIZE = 100; } { version = 1; } Example(String name) { this.name = name; } }
  • static final 修饰的字段必须在声明处或静态初始化块中完成初始化;
  • final 实例字段必须在声明处、实例初始化块,或者每一条构造路径中完成初始化;
  • 每个空白final字段必须在规定的初始化阶段完成一次赋值,并且不能在后续代码中再次赋值;
  • 编译器会通过“确定赋值分析”检查变量是否在使用前完成赋值,以及是否可能被重复赋值;

3.4 修饰基本类型变量和引用类型变量

final限制的是变量的再次赋值,而不是对象的可变性。对于引用变量,不能改变它保存的引用,但被引用对象的内部状态仍然可能改变。

基本类型变量 number 保存数值10;引用类型变量names保存ArrayList对象的引用。final对两者的约束都是禁止再次赋值,但具体表现有所不同。

用一句话总结概括就是:"final限制的是变量不能被再次赋值。对于引用类型变量,变量中保存的对象引用不能被替换,但该引用指向的对象仍然可能是可变的";

final int number = 10; // number = 20; // 不允许 final List<String> names = new ArrayList<>(); names.add("Alice"); // 允许修改对象状态 // names = new ArrayList<>(); // 不允许更换引用

3.5 修饰局部变量

result 虽然没有在声明时初始化,但每条分支都恰好赋值一次,因此可以通过编译;

void process(boolean success) { final int result; if (success) { result = 1; } else { result = 0; } System.out.println(result); }

3.6 修饰方法参数

其实和上面基本类型变量、引用类型变量的规则差不多

方法参数也可以为 final,但它只能防止方法内部重新给参数赋值,不能阻止外部对象被修改:

void update(final String name) { // name = "newName"; // 不允许重新赋值 } void update(final List<String> names) { names.add("Alice"); // 允许 // names = new ArrayList<>(); // 不允许 }

3.7 final 不保证线程安全

道理很简单,final 只保证对象的引用不可变,但是高并发场景下,引用的对象比如集合进行并发 add() 修改,是线程不安全的;

3.8 static final 修饰的不一定是编译期常量

只有使用常量表达式初始化的基本类型或 String 类型的 static final 字段,才属于编译期常量

static final int MAX_SIZE = 100; // 编译期常量 static final Integer RANDOM = new Random().nextInt(); // 不是编译期常量
http://www.jsqmd.com/news/1201696/

相关文章:

  • HarmonyOS时光胶囊——统计页面的月历与心情可视化
  • 2024年Eclipse精准配置指南:JDK兼容、Tomcat集成与unresolved inclusion修复
  • Voyeur.js API深度解析:掌握find、use、eq等核心方法
  • Qwopus3.6-35B-A3B-Coder-6bit模型终极指南:6位量化如何让AI编码效率提升3倍?
  • 2026大湾区EMBA择校测评:小班教学高性价比榜单,民企老板避坑指南
  • 正规名录:福州灯光音响租赁机构TOP10精选|2026年度最新测评结果全流程服务标准与价格透明说明及选型参考完整指南 - 影视产业研究
  • 分布式追踪的“存不起“困境:固定采样、概率采样与尾部采样,你的采样策略决定了排障效率的上限
  • tRPC.panel()架构解析:深入理解自动UI生成原理
  • 从理论到生产:零知识证明在公链扩容中的落地瓶颈与突破
  • 2026西安办公室除甲醛除异味公司哪家专业 - 西安治泉环保
  • 用AI识别项目范围的隐性膨胀(Scope Creep)模式:从被动应对到主动预警
  • Unity Hub启动卡顿?修改JS文件实现秒级启动的硬核方案
  • MysQL的binlog三种录入格式详解
  • synchronized 锁的用法
  • 毕设开源 深度学习情感分类算法系统(源码+论文)
  • googletest简介gtest, ut, 加过滤条件EXPECT_CALL
  • rabbitmq之工作队列(一)
  • 湖北现代科技学校咨询热线是多少 - 湖北找学校
  • 公考机构课程有效期陷阱:那些你没注意到的“隐形条款“
  • Crypto+AI为什么叫好不叫座?供需错配背后的基础设施困局
  • FanControl风扇控制软件:3步解决华硕主板传感器识别限制的高效方案
  • 【第九天】C++函数对象/仿函数、谓词、适配器及常见algorithm算法
  • DAC单端转差分电路设计与应用指南
  • MiniCPM5-1B-Agentic-Tooluse-GGUF与vLLM集成指南:实验性GGUF支持配置教程
  • 技术架构文档的写作规范:ADRs(Architecture Decision Records)从原理到实践
  • Redis 3.x——哨兵基本概念
  • 如何在Apple Silicon上部署Qwopus3.6-35B-A3B-Coder-6bit:从安装到运行的完整教程
  • Rainbow-delimiters性能测试报告:与其他彩虹括号插件的对比
  • AI产品用户旅程的转化漏斗模型:从首次激活到深度留存的工程化分析
  • 小程序计算机毕设之文旅展馆线上预约服务系统的设计与实现 基于 Django + 微信小程序的咖啡博物馆票务管理小程序(完整前后端代码+说明文档+LW,调试定制等)