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

许多编程语言都会有某种方法来告知编译器,某一块数据是恒定不变的。有时候恒定不变的数据很有用,比如

  • 一个永不改变的编译期常量 。例如 static final int num = 1024
  • 一个运行时被初始化的值,而且你不希望改变它

final 的设计会和 abstract 的设计产生冲突,因为 abstract 关键字主要修饰抽象类,而抽象类需要被具体的类所实现。final 表示禁止继承,也就不会存在被实现的问题。因为只有继承后,子类才可以实现父类的方法。

类中的所有 private 都隐式的指定为 final 的,在 private 修饰的代码中使用 final 并没有额外的意义。

空白 final

Java 是允许空白 final 的,空白 final 指的是声明为 final ,但是却没有对其赋值使其初始化。但是无论如何,编译器都需要初始化 final,所以这个初始化的任务就交给了构造器来完成,空白 final 给 final 提供了更大的灵活性。如下代码

public class FinalTest {

final Integer finalNum;

public FinalTest(){
finalNum = 11;
}

public FinalTest(int num){
finalNum = num;
}

public static void main(String[] args) {
new FinalTest();
new FinalTest(25);
}
}

在不同的构造器中对不同的 final 进行初始化,使 finalNum 的使用更加灵活。

使用 final 的方法主要有两个:不可变效率

  • 不可变:不可变说的是把方法锁定(注意不是加锁),重在防止其他方法重写。
  • 效率:这个主要是针对 Java 早期版本来说的,在 Java 早期实现中,如果将方法声明为 final 的,就是同意编译器将对此方法的调用改为内嵌调用,但是却没有带来显著的性能优化。这种调用就比较鸡肋,在 Java5/6 中,hotspot 虚拟机会自动探测到内嵌调用,并把它们优化掉,所以使用 final 修饰的方法就主要有一个:不可变。

注意:final 不是 Immutable 的,Immutable 才是真正的不可变。

final 不是真正的 Immutable,因为 final 关键字引用的对象是可以改变的。如果我们真的希望对象不可变,通常需要相应的类支持不可变行为,比如下面这段代码

final List fList = new ArrayList();
fList.add(“Hello”);
fList.add(“World”);
List unmodfiableList = List.of(“hello”,“world”);
unmodfiableList.add(“again”);

List.of 方法创建的就是不可变的 List。不可变 Immutable 在很多情况下是很好的选择,一般来说,实现 Immutable 需要注意如下几点

  • 将类声明为 final,防止其他类进行扩展。
  • 将类内部的成员变量(包括实例变量和类变量)声明为 privatefinal 的,不要提供可以修改成员变量的方法,也就是 setter 方法。
  • 在构造对象时,通常使用 deep-clone ,这样有助于防止在直接对对象赋值时,其他人对输入对象的修改。
  • 坚持 copy-on-write 原则,创建私有的拷贝。

final 能提高性能吗?

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

大致说的就是无论局部变量声明时带不带 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 方法,会发现如下字节码

左边是非 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,为啥呢?

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

字节码的中文注释我已经给你标出来了,这里需要注意一下下面的 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 块是没有异常表的,下面来验证一下

2021年Java中高级面试必备知识点总结

在这个部分总结了2019年到目前为止Java常见面试问题,取其面试核心编写成这份文档笔记,从中分析面试官的心理,摸清面试官的“套路”,可以说搞定90%以上的Java中高级面试没一点难度。

本节总结的内容涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。

目录:

(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)

部分内容:

对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。

不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注

部分内容:

[外链图片转存中…(img-ttPDVXct-1714765637753)]

[外链图片转存中…(img-YHugZbD1-1714765637753)]

[外链图片转存中…(img-94QKVHuF-1714765637753)]

对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。

不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值