面试考察点
考察目的: 了解求职者对Java基础的了解。
考察范围: 工作1-3年的Java程序员。
背景知识
final/finally在工作中几乎无时无刻不再使用,因此即便是没有系统化的梳理这个问题,也能回答出一些内容。
但是finalize就接触得非常少,接下来我们对这几个关键字逐一进行分析。
final关键字
final关键字代表着不可变性。
finally关键字
finally关键字用在try语句块后面,它的常用形式是
try{
}catch(){
}finally{
}
 
以及下面这种形式。
try{
}finally{
}
 
 
它的特点:
finally语句一定会伴随try语句出现。try语句不能单独使用,必须配合catch语句或finally语句。try语句可以单独与catch语句一起使用,也可以单独与finally语句一起使用,也可以三者一起使用。
finally 实战思考
为了加深大家对于finally关键字的理解,我们来看下面这段代码。
public class FinallyExample {
    public static void main(String arg[]){
        System.out.println(getNumber(0));
        System.out.println(getNumber(1));
        System.out.println(getNumber(2));
        System.out.println(getNumber(4));
    }
    public static int getNumber(int num){
        try{
            int result=2/num;
            return result;
        }catch(Exception exception){
            return 0;
        }finally{
            if(num==0){
                return -1;
            }
            if(num==1){
                return 1;
            }
        }
    }
}
 
正确答案分别是:
-1: 传入num=0,此时会报错java.lang.ArithmeticException: / by zero。因此进入到catch捕获该异常。由于finally语句块一定会被执行,因此进入到finally语句块,返回-1。1:传入num=1,此时程序运行正常,由于finally语句块一定会被执行,因此进入到finally代码块,得到结果1。1:传入num=2,此时程序运行正常,result=1,由于finally语句块一定会被执行,因此进入到finally代码块,但是finally语句块并没有触发对结果的修改,所以返回结果为1。0:传入num=4,此时程序运行正常,result=0(因为2/4=0.5,转换为int后得到0),由于finally语句块一定会被执行,因此进入到finally代码块,但是finally语句块并没有触发对结果的修改,所以返回结果为0。
什么情况下finally不会执行
 
finally代码块,是否有存在不会被执行的情况呢?
System.exit()
来看下面这段代码:
public class FinallyExample {
    public static void main(String arg[]){
        System.out.println(getNumber(0));
    }
    public static int getNumber(int num){
        try{
            int result=2/num;
            return result;
        }catch(Exception exception){
            System.out.println("触发异常执行");
            System.exit(0);
            return 0;
        }finally{
            System.out.println("执行finally语句块");
        }
    }
}
 
在catch语句块中,增加了System.exit(0)代码,执行结果如下
触发异常执行
 
可以发现,在这种情况下,并没有执行finally语句块。
由于当前JVM已经结束了,因此程序代码自然不能继续执行。
守护线程被中断
先来看下面这段代码:
public class FinallyExample {
    public static void main(String[] args) {
        Thread t = new Thread(new Task());
        t.setDaemon(true); //置为守护线程
        t.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException("the "+Thread.currentThread().getName()+" has been interrupted",e);
        }
    }
}
class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("执行 run()方法");
        try {
            System.out.println("执行 try 语句块");
            TimeUnit.SECONDS.sleep(5); //阻塞5s
        } catch(InterruptedException e) {
            System.out.println("执行 catch 语句块");
            throw new RuntimeException("the "+Thread.currentThread().getName()+" has been interrupted",e);
        } finally {
            System.out.println("执行 finally 语句块");
        }
    }
}
 
运行结果如下:
执行 run()方法
执行 try 语句块
 
从结果发现,finally语句块中的代码并没有被执行?为什么呢?
在上述运行的程序中,执行逻辑描述如下:
- 线程
t是守护线程,它开启一个任务Task执行,该线程t在main方法中执行,并且在睡眠1s之后,main方法执行结束 Task是一个守护线程的执行任务,该任务睡眠5s。
基于守护线程的特性,main和task都是守护线程,因此当main线程执行结束后,并不会因为Task这个线程还未执行结束而阻塞。而是在等待1s后,结束该进程。
这就使得Task这个线程的代码还未执行完成,但是JVM进程已结束,所以finally语句块没有被执行。
finally执行顺序
基于上述内容的理解,是不是自认为对finally关键字掌握很好了?那我们在来看看下面这个问题。
public class FinallyExample2 {
  public int add() {
    int x = 1;
    try {
      return ++x;
    } catch (Exception e) {
      System.out.println("执行catch语句块");
      ++x;
    } finally {
      System.out.println("执行finally语句块");
      ++x;
    }
    return x;
  }
  public static void main(String[] args) {
    FinallyExample2 t = new FinallyExample2();
    int y = t.add();
    System.out.println(y);
  }
}
 
上述程序运行的结果是:2
这个结果应该有点意外,因为按照finally的语义,首先执行try代码块,++x后得到的结果应该是2, 接着再执行finally语句块,应该是在2的基础上再+1,得到结果是3,那为什么是2?
在解答这个问题之前,先来看一下这段代码的字节码,使用javap -v FinallyExample2.
 public int add();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1     //iconst 指令将常量压入栈中。
         1: istore_1     //
         2: iinc          1, 1  //执行++x操作
         5: iload_1       
         6: istore_2
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #3                  // String 执行finally语句块
        12: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: iinc          1, 1
        18: iload_2
        19: ireturn
        20: astore_2
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: ldc           #6                  // String 执行catch语句块
        26: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        29: iinc          1, 1
        32: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        35: ldc           #3                  // String 执行finally语句块
        37: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        40: iinc          1, 1
        43: goto          60
        46: astore_3
        47: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        50: ldc           #3                  // String 执行finally语句块
        52: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        55: iinc          1, 1
        58: aload_3
        59: athrow
        60: iload_1
        61: ireturn
      Exception table:
         from    to  target type
             2     7    20   Class java/lang/Exception
             2     7    46   any
            20    32    46   any
 
简单说明一下和本次案例分析有关的字节指令
iconst,把常量压入到栈中。istore,栈顶的int数值存入局部变量表。iload,把int类型的变量压入到栈顶。iinc,对局部变量表中index为i的元素加上n。ireturn,返回一个int类型的值。astore,将一个数值从操作数栈存储到局部变量表。athrow,抛出一个异常。aload,将一个局部变量加载到操作栈。
了解了这些指令之后,再来分析上述字节码的内容。
先来看第一步分,这部分是try代码块中的指令。
0: iconst_1     //iconst 指令将常量压入栈中。
1: istore_1     //
2: iinc          1, 1  //执行++x操作
5: iload_1       
6: istore_2
 
上述指令的执行流程,图解如下。

接下来继续往下看字节码,这个是在finally里面执行的指令。
15: iinc          1, 1
18: iload_2
19: ireturn
20: astore_2
 

从上述指令的图解过程中可以看到,在finally语句块中虽然对x的值做了累加,但是最终返回的时候,仍然是2.
后续剩余的指令,是异常表对应的执行指令,异常表的解读方式是:
-  
从2行到第7行,如果触发了Exception,则会跳转到20行的指令开始执行。
 -  
从2行到第7行,如果触发了任何异常,则会跳转到46行开始执行。
 -  
从20行到第32行,如果触发了任何异常,则会跳转到46行开始执行。
 
Exception table:
  from    to  target type
    2     7    20   Class java/lang/Exception
    2     7    46   any
    20    32    46   any
 
 
除此之外,还有其他的变体,比如:
public class FinallyExample2 {
    public int add() {
        int x = 1;
        try {
            return ++x;
        } catch (Exception e) {
            System.out.println("执行catch语句块");
            ++x;
        } finally {
            System.out.println("执行finally语句块");
            ++x;
            return x;
        }
    }
    public static void main(String[] args) {
        FinallyExample2 t = new FinallyExample2();
        int y = t.add();
        System.out.println(y);
    }
}
 
那,这段代码运行结果是多少呢?
打印结果如下:
执行finally语句块
3
 
 
 
简单翻译如下:
如果 try 语句里有 return,那么代码的行为如下:
- 如果有返回值,就把返回值保存到局部变量中
 - 执行 jsr 指令跳到 finally 语句里执行
 - 执行完 finally 语句后,返回之前保存在局部变量表里的值
 
finalize方法
finalize 方法定义在 Object 类中,其方法定义如下:
protected void finalize() throws Throwable {
}
 
当一个类在被回收期间,这个方法就可能会被调用到。
它有使用规则是:
- 当对象不再被任何对象引用时,GC会调用该对象的finalize()方法
 - finalize()是Object的方法,子类可以覆盖这个方法来做一些系统资源的释放或者数据的清理
 - 可以在finalize()让这个对象再次被引用,避免被GC回收;但是最常用的目的还是做cleanup
 - Java不保证这个finalize()一定被执行;但是保证调用finalize的线程没有持有任何user-visible同步锁。
 - 在finalize里面抛出的异常会被忽略,同时方法终止。
 - 当finalize被调用之后,JVM会再一次检测这个对象是否能被存活的线程访问得到,如果不是,则清除该对象。也就是finalize只能被调用一次;也就是说,覆盖了finalize方法的对象需要经过两个GC周期才能被清除。
 
问题回答
回答:
-  
final用来修饰类、方法、属性,被final修饰的类,表示该类无法被继承,被final修饰的属性,表示该属性无法被修改,被final修饰的方法,表示该方法无法被重写
 -  
finally,它和try语句块组成一个完整的语法,表示一定会被执行的代码块,当然也有方式可以破坏它的执行特性
- 通过System.exit
 - 守护线程的终止
 
 -  
finalize方法,是一个类被回收期间可能会被调用的方法。
 
问题总结
一道面试题,要深挖下来,可以产生很多变体。
这篇文章不一定非常全面的涵盖了所有可能的情况,但是各位读者一定要注意,只有体系化的知识,才能创造价值










