看完这篇 final、finally 和 finalize 和面试官扯皮就没问题了(二)

简介: final 是 Java 中的关键字,它也是 Java 中很重要的一个关键字,final 修饰的类、方法、变量有不同的含义;finally 也是一个关键字,不过我们可以使用 finally 和其他关键字结合做一些组合操作;finalize 是一个不让人待见的方法,它是对象祖宗 Object 中的一个方法,finalize 机制现在已经不推荐使用了。本篇文章,cxuan 就带你从这三个关键字入手,带你从用法、应用、原理的角度带你深入浅出理解这三个关键字。

final 能提高性能吗?


final 能否提高性能一直是业界争论的点,很多书籍中都介绍了可以在特定场景提高性能,例如 final 可能用于帮助 JVM 将方法进行内联,可以改造编译器进行编译的能力等等,但这些结论很多都是基于假设作出的。

或许 R 大这篇回答会给我们一些结论 https://www.zhihu.com/question/21762917

大致说的就是无论局部变量声明时带不带 final 关键字修饰,对其访问的效率都一样

比如下面这段代码(不带 final 的版本)

static int foo() {
  int a = someValueA();
  int b = someValueB();
  return a + b; // 这里访问局部变量
}

带 final 的版本

static int foo() {
  final int a = someValueA();
  final int b = someValueB();
  return a + b; // 这里访问局部变量
}

使用 javac 编译后得出来的结果一摸一样。

invokestatic someValueA:()I
istore_0 // 设置a的值
invokestatic someValueB:()I
istore_1 // 设置b的值
iload_0  // 读取a的值
iload_1  // 读取b的值
iadd
ireturn

因为上面是使用引用类型,所以字节码相同。

如果是常量类型,我们看一下

// 带 final
static int foo(){
  final int a = 11;
  final int b = 12;
  return a + b;
}
// 不带 final
static int foo(){
  int a = 11;
  int b = 12;
  return a + b;
}

我们分别编译一下两个 foo 方法,会发现如下字节码

25.png

左边是非 final 关键字修饰的代码,右边是有 final 关键字修饰的代码,对比这两个字节码,可以得出如下结论。

  • 不管有没有 final 修饰 ,int a = 11 或者 int a = 12 都当作常量看待。
  • 在 return 返回处,不加 final 的 a + b 会当作变量来处理;加 final 修饰的 a + b 会直接当作常量处理。

其实这种层面上的差异只对比较简易的 JVM 影响较大,因为这样的 VM 对解释器的依赖较大,原本 Class 文件里的字节码是怎样的它就怎么执行;对高性能的 JVM(例如 HotSpot、J9 等)则没啥影响。

所以,大部分 final 对性能优化的影响,可以直接忽略,我们使用 final 更多的考量在于其不可变性。


深入理解 finally


我们上面大致聊到了 finally 的使用,其作用就是保证在 try 块中的代码执行完成之后,必然会执行 finally 中的语句。不管 try 块中是否抛出异常。

那么下面我们就来深入认识一下 finally ,以及 finally 的字节码是什么,以及 finally 究竟何时执行的本质。

  • 首先我们知道 finally 块只会在 try 块执行的情况下才执行,finally 不会单独存在

这个不用再过多解释,这是大家都知道的一条规则。finally 必须和 try 块或者 try catch 块一起使用。

  • 其次,finally 块在离开 try 块执行完成后或者 try 块未执行完成但是接下来是控制转移语句时(return/continue/break)在控制转移语句之前执行

这一条其实是说明 finally 的执行时机的,我们以 return 为例来看一下是不是这么回事。

如下这段代码

static int mayThrowException(){
  try{
    return 1;
  }finally {
    System.out.println("finally");
  }
}
public static void main(String[] args) {
  System.out.println(FinallyTest.mayThrowException());
}

从执行结果可以证明是 finally 要先于 return 执行的。

当 finally 有返回值时,会直接返回。不会再去返回 try 或者 catch 中的返回值。

static int mayThrowException(){
  try{
    return 1;
  }finally {
    return 2;
  }
}
public static void main(String[] args) {
  System.out.println(FinallyTest.mayThrowException());
}
  • 在执行 finally 语句之前,控制转移语句会将返回值存在本地变量中

看下面这段代码

static int mayThrowException(){
  int i = 100;
  try {
    return i;
  }finally {
    ++i;
  }
}
public static void main(String[] args) {
  System.out.println(FinallyTest.mayThrowException());
}

上面这段代码能够说明 return i 是先于 ++i 执行的,而且 return i 会把 i 的值暂存,和 finally 一起返回。


finally 的本质


下面我们来看一段代码

public static void main(String[] args) {
  int a1 = 0;
  try {
    a1 = 1;
  }catch (Exception e){
    a1 = 2;
  }finally {
    a1 = 3;
  }
  System.out.println(a1);
}

这段代码输出的结果是什么呢?答案是 3,为啥呢?

抱着疑问,我们先来看一下这段代码的字节码

26.png

字节码的中文注释我已经给你标出来了,这里需要注意一下下面的 Exception table,Exception table 是异常表,异常表中每一个条目代表一个异常发生器,异常发生器由 From 指针,To 指针,Target 指针和应该捕获的异常类型构成。

所以上面这段代码的执行路径有三种

  • 如果 try 语句块中出现了属于 exception 及其子类的异常,则跳转到 catch 处理
  • 如果 try 语句块中出现了不属于 exception 及其子类的异常,则跳转到 finally 处理
  • 如果 catch 语句块中新出现了异常,则跳转到 finally 处理

聊到这里,我们还没说 finally 的本质到底是什么,仔细观察一下上面的字节码,你会发现其实 finally 会把 a1 = 3 的字节码 iconst_3 和 istore_1 放在 try 块和 catch 块的后面,所以上面这段代码就形同于

public static void main(String[] args) {
  int a1 = 0;
  try {
    a1 = 1;
  // finally a1 = 3
  }catch (Exception e){
    a1 = 2;
    // finally a1 = 3
  }finally {
    a1 = 3;
  }
  System.out.println(a1);
}

上面中的 Exception table 是只有 Throwable 的子类 exception 和 error 才会执行异常走查的异常表,正常情况下没有 try  块是没有异常表的,下面来验证一下

public static void main(String[] args) {
  int a1 = 1;
  System.out.println(a1);
}

比如上面我们使用了一段非常简单的程序来验证,编译后我们来看一下它的字节码

27.png

可以看到,果然没有异常表的存在。


finally 一定会执行吗


上面我们讨论的都是 finally 一定会执行的情况,那么 finally 一定会被执行吗?恐怕不是。

除了机房断电、机房爆炸、机房进水、机房被雷劈、强制关机、拔电源之外,还有几种情况能够使 finally 不会执行。

  • 调用 System.exit 方法
  • 调用 Runtime.getRuntime().halt(exitStatus) 方法
  • JVM 宕机(搞笑脸)
  • 如果 JVM 在 try 或 catch 块中达到了无限循环(或其他不间断,不终止的语句)
  • 操作系统是否强行终止了 JVM 进程;例如,在 UNIX 上执行 kill -9 pid
  • 如果主机系统死机;例如电源故障,硬件错误,操作系统死机等不会执行
  • 如果 finally 块由守护程序线程执行,那么所有非守护线程在 finally 调用之前退出。


finalize 真的没用吗


我们上面简单介绍了一下 finalize 方法,并说明了它是一种不好的实践。那么 finalize 调用的时机是什么?为什么说 finalize 没用呢?

我们知道,Java 与 C++ 一个显著的区别在于 Java 能够自动管理内存,在 Java 中,由于 GC 的自动回收机制,因而并不能保证 finalize 方法会被及时地执行(垃圾对象的回收时机具有不确定性),也不能保证它们会被执行。

也就是说,finalize 的执行时期不确定,我们并不能依赖于 finalize 方法帮我们进行垃圾回收,可能出现的情况是在我们耗尽资源之前,gc 却仍未触发,所以推荐使用资源用完即显示释放的方式,比如 close 方法。除此之外,finalize 方法也会生吞异常。

finalize 的工作方式是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将会首先调用 finalize 方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。垃圾回收只与内存有关

我们在日常开发中并不提倡使用 finalize 方法,能用 finalize 方法的地方,使用 try...finally 会处理的更好。

相关文章
|
19天前
|
Java 程序员
Java 面试高频考点:static 和 final 深度剖析
本文介绍了 Java 中的 `static` 和 `final` 关键字。`static` 修饰的属性和方法属于类而非对象,所有实例共享;`final` 用于变量、方法和类,确保其不可修改或继承。两者结合可用于定义常量。文章通过具体示例详细解析了它们的用法和应用场景。
22 3
|
3月前
|
存储 Java 对象存储
【Java基础面试四十三】、 static和final有什么区别?
由于网络原因,我无法获取到您提供的链接内容。如果需要我解析该网页,请确保链接有效并重试,或者提供其他问题,我会尽力帮助您。
|
6月前
|
Java
【JAVA面试题】final关键字的作用有哪些
【JAVA面试题】final关键字的作用有哪些
|
6月前
|
Java
面试官:小伙子来说一说Java中final关键字,以及它和finally、finalize()有什么区别?
面试官:“小伙子,用过final关键字吗?” 我:“必须用过呀” 面试官:“好,那来说一说你对这个关键字的理解吧,再说一说它与finally、finalize()的区别” 我:“好嘞!
42 1
|
Java API
每日一道面试题之final、finally、finalize 有什么区别?
每日一道面试题之final、finally、finalize 有什么区别?
|
Java 编译器
【Java面试】为什么匿名内部类只能访问外部类的final类型局部变量?
【Java面试】为什么匿名内部类只能访问外部类的final类型局部变量?
149 0
|
6月前
|
Java
【面试问题】final 和可以保证可见性吗?
【1月更文挑战第27天】【面试问题】final 和可以保证可见性吗?
每天一个面试题之final在java中有什么作用?
每天一个面试题之final在java中有什么作用?
|
存储 设计模式 编译器
【C++的多态】多态的概念|| 多态的定义及实现||多态的原理||多态的构成条件||虚函数表||C++11 override 和 final ||重载、覆盖(重写)、隐藏(重定义)的对比||常见面试题
本篇将讲述多态的概念、多态的定义及实现、抽象类、多态的原理、单继承和多继承关系中的虚函数表以及继承和多态常见的面试问题 等。
124 0
|
Java 数据库连接
Java 最常见的面试题:hibernate 实体类可以被定义为 final 吗?
Java 最常见的面试题:hibernate 实体类可以被定义为 final 吗?
107 0