简单介绍

我想每一个学习java的同学都一定遇到过这样一道经典的面试题:谈谈java中final,finally,finalize的区别和用法。面对这道题,我想每个人或多或少都能说出他们各自的功效。确实,这三个虽然长得很像,但是作用却是完全不相同的。不过,今天的重点是finally,所以还不太明白的同学建议自行度娘,这里不再赘述。

题外话:finalize在Java9中已经被标注为过时的方法,可以使用java.lang.ref.Cleanerjava.lang.ref.PhantomReference进行替代。

finally表示总是执行。放入finally块中的代码,无论是否有异常发生,都会执行,他是Java对异常处理的最佳补充。所以我们常用来做数据库连接的关闭,资源的释放等操作。例如:

public static void main(String[] args) {
        String sql = "select * from user";
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = DriverManager.getConnection("mysqlUrl", "root", "123456");
            ps =  con.prepareStatement(sql);
            ps.executeQuery();
        }catch (SQLException e) {
            e.printStackTrace();
        }finally {
            try {
                if(con != null){
                    con.close();
                }
                if(ps != null){
                    ps.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
}
public static void main(String[] args) {
        String file = "D:\\test.txt";
        FileInputStream is = null;
        try {
            is = new FileInputStream(file);
            int i = 0;
            while ((i = is.read()) != -1) {
                System.out.print((char) i);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(is != null){
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
}

这两段代码demo就很好的诠释了finally的作用,避免在资源使用或者发生异常之后无法关闭,造成资源无法释放。

try-with-resource语法

还是上面的两个例子,可以看出,虽然进行了手动的资源关闭,但是在代码中使用了太多的try...catch...块,不仅代码阅读不方便,而且感觉也很麻烦。我们不仅要问,难道就不能有一种方法让资源自动关闭么。
基于此,try-with-resource语法诞生了。实际上,这是java7中提供的语法糖,将资源对象的创建放在try语句之后的()中,这样当try...catch...代码块执行完毕后,Java会自动调用资源的close方法。根据描述,我们可以对上述两个例子进行改造,改造之后的代码为:

public static void main(String[] args) {
        String sql = "select * from user";
        try(Connection con = DriverManager.getConnection("mysqlUrl", "root", "123456");
            PreparedStatement ps =  con.prepareStatement(sql)){
            ps.executeQuery();
        }catch (SQLException e) {
            e.printStackTrace();
        }
}
public static void main(String[] args) {
        String file = "D:\\test.txt";
        try(FileInputStream is = new FileInputStream(file)){
            int i = 0;
            while ((i = is.read()) != -1) {
                System.out.print((char) i);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
}

对比一下改造前后有没有发现代码看起来非常的简洁明了。
这里必须注意一下的是要想使用try-with-resource语法,该资源对象(例如demo中FileInputStream和Connection)必须实现AutoCloseable接口或者Closeable接口。

实际上Closeable接口也继承了AutoCloseable接口。

当有多个资源对象创建时,可以用;分割。

那么try-with-resource语法是如何实现的自动关闭呢,我们对改造的第二段代码进行反编译:

public static void main(String[] args){
        String file = "D:\\test.txt";
        try{
            FileInputStream is = new FileInputStream(file);
            Throwable localThrowable3 = null;
            try{
                int i = 0;
                while ((i = is.read()) != -1) {
                    System.out.print((char)i);
                }
            }catch (Throwable localThrowable1){
                localThrowable3 = localThrowable1;
                throw localThrowable1;
            }finally{
                if (is != null) {
                    if (localThrowable3 != null) {
                        try{
                            is.close();
                        }catch (Throwable localThrowable2){
                            localThrowable3.addSuppressed(localThrowable2);
                        }
                    } else {
                        is.close();
                    }
                }
            }
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
}

可以看出,其实依旧是使用了finally块来做资源的关闭。只不过减少了程序员的工作量,而且代码更加简洁美观。

异常抑制

在反编译代码中,有这样一行代码:localThrowable3.addSuppressed(localThrowable2); 这是在java7中为Throwable类增加的方法,这里涉及到一个概念,叫做异常抑制。
当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过addSuppressed方法把这些被抑制的方法记录下来。被抑制的异常会出现在抛出的异常的堆栈信息中,可以通过getSuppressed方法来获取这些异常。这样做的好处是不会丢失任何异常,方便开发人员进行调试。

return与finally

上面我们说过,finally块的代码是一定会执行的,而return也是终止当前方法。那么他们的先后顺序是怎样的呢。

public class Test {
    public static void main(String[] args) {
        String name1 = test1("王五");
        System.out.println("test1运行结果为:" + name1);
        String name2 = test2("王五");
        System.out.println("test2运行结果为:" + name2);
        String name3 = test3("王五");
        System.out.println("test3运行结果为:" + name3);
    }

    public static String test1(String name){
        try {
            name = "张三";
            return name;
        }finally {
            name = "李四";
            System.out.println("test1方法finally语句块name:"+name);
        }
    }

    public static String test2(String name){
        try {
            name = "张三";
        }finally {
            name = "李四";
            System.out.println("test2方法finally语句块name:"+name);
            return name;
        }
    }

    public static String test3(String name){
        try {
            name = "张三";
            return name;
        }finally {
            name = "李四";
            System.out.println("test3方法finally语句块name:"+name);
            return name;
        }
    }
}

上述代码的运行结果为:

test1方法finally语句块name:李四
test1运行结果为:张三
test2方法finally语句块name:李四
test2运行结果为:李四
test3方法finally语句块name:李四
test3运行结果为:李四

可以看出,finally语句块中的代码确实执行了,但是虽然在finally中更改了返回值,不过最后的返回值完全取决于return最终出现的位置。
所以finally执行是在return之前执行的。而return的执行又是分为两部分的,一部分是return指令,一部分是return后面的表达式expression。
他们三者关联在一起实际的执行顺序是:

1、执行:expression,计算该表达式,结果保存在操作数栈顶;
2、执行:操作数栈顶值(expression的结果)复制到局部变量区作为返回值;
3、执行:finally语句块中的代码;
4、执行:将第2步复制到局部变量区的返回值又复制回操作数栈顶;
5、执行:return指令,返回操作数栈顶的值;