关于finally的一些思考

对于try catch每一个开发者都很熟悉,那么如果深究一下他们的执行顺序,提前中断,提前返回那么是怎么样一个结果呢

前排提示不想看原理最后有结果总结

关于try catch比较浅显一点的理解就是try包裹有可能出错的代码,catch对可能出现的错误进行捕获,finally处理一些资源回收的操作。

那么提出这样一个问题,如果try的时候retuen了,finally的内容还会执行吗。

public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		t.fun();
	}
	public String fun() {
		try {
			System.out.println("111");
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}
}
111
222

可以看到finally里面的内容执行了。
那么我们提出第二个问题

finally的执行时在return之前还是return之后

首先是关于return返回的底层知识

java方法是在栈帧中执行,栈帧是线程私有栈的单位,执行方法的线程会为每一个方法分配一小块栈空间来作为该方法执行时的内存空间,栈帧分为三个区域:
1. 操作数栈,用来保存正在执行的表达式中的操作数,数据结构中学习过基于栈的多项式求值算法,操作数栈的作用和这个一样
2. 局部变量区,用来保存方法中使用的变量,包括方法参数,方法内部声明的变量,以及方法中使用到的对象的成员变量或类的成员变量(静态变量),最后两种变量会复制到局部变量区,因此在多线程 环境下,这种变量需要根据需要声明为volatile类型
3. 字节码指令区,这个不用解释了,就是方法中的代码翻译成的指令

Java官网的文档说明
If the try clause executes a return, the compiled code does the following:
Saves the return value (if any) in a local variable.
Executes a jsr to the code for the finally clause.
Upon return from the finally clause, returns the value saved in the local variable.
翻译:
如果try块中有return,代码会做以下工作:
1、将return的值保存在一个本地变量中;
2、执行finally块中的Java Specification Requests(jsr,java标准提案)代码;
3、执行完finally块后(从finally中返回后),return刚才保存在本地变量中的值。
先执行return return语句先把返回值写入到内存中。然后停下来等待finally语句块执行完,return再执行后面的一段。
这个是重中之重!!!敲黑板做笔记!!!

public class TestTryCatch {
    public static void main(String[] args)
    {
        TestTryCatch test = new TestTryCatch();
        System.out.println(test.fun());
    }

    public int fun()
    {
        int i = 10;
        try
        {
            //doing something
            return i;
        }catch(Exception e){
            return i;
        }finally{
            i = 20;
        }
    }
}

结果为10,创建了一个方法fun,在方法里使用try/catch语句,方法要求返回值类型为int型。在try里面放回i,这个时候是10,但是在finally里面将i值修改为20。我们看到结果是10,好像是return先执行。那么接下来再看另一个例子:

public class TestTryCatch {
    public static void main(String[] args)
    {
        TestTryCatch test = new TestTryCatch();
        System.out.println(test.fun());
    }

    public StringBuilder fun()
    {
        StringBuilder s = new StringBuilder("Hello");
        try
        {
            //doing something
            s.append("Word");

            return s;
        }catch(Exception e){
            return s;
        }finally{
            string.append("finally");
        }
    }
}

输出结果:HelloWordFinally
看结果似乎有点出乎意料了,因为这次finally里面修改的内容生效了。看代码其实差别不大,只是把返回值类型修改为StringBuilder了。那么这是为什么呢?下面就为大家解释一下其中到底是怎么执行的。
首先,拿第一个例子来说,可以在main方法里实现这样一条语句:int result = test.fun();我们知道这样做是没有问题的,但是大家都知道“=”号赋值是常量赋值。但是,方法的存放地址和常量的存放地址是不一样的,方法的存放在代码区的。上面我们把一个方法赋值给一个int型也没有报错。那是因为在声明方法是我们声明了返回值类型。那么编译器就会在代码的最前端预留一段返回值类型的内存。执行return的时候,就会把返回的内容写入到这段内存中
在这里插入图片描述
这样,执行“=”号赋值的时候,就能在内存中匹配到相同的类型。赋值便能成功。

弄清楚上面的道理之后,再来解释最开始提出的问题就容易多了。在执行了return之后,返回的值已经被写入到那段内存中了,finally再修改i的值,只是修改了后面代码段的i值,对返回段内存没有影响。至于第二个例子,内存地址是一样的,在字符串后面追加了内容,在return返回的时候一并返回。
当返回值不是基本数据类型的时候,其是指向一段内存的,return将返回段指向一段内存,但是代码段的s依然是指向的同一段内存地址,所以当s修改它指向内存中的值的时候,其实也就修改了返回段指向内存中的值,所以最终的值改变。
下面有一个栗子自行阅读,会有助于理解return和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运行结果为:李四

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

我们观察到finally都执行了,那么提出一个问题,什么情况下,finally不会执行呢

当然不能说断电就不执行了,现在提出这个几个观点来验证
1.报运行时异常的时候finally会执行吗
2.手动让程序结束System.exit(0);之前写算法的时候粗浅的用过这个函数暴力让程序停止
3.直接炸栈的运行时异常
ps:其实主要是第二个,1,3也是异常,会按照try catch处理异常的正常流程走的

//1.报运行时异常的时候finally会执行吗
public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		t.fun();
	}
	public String fun() {
		try {
			System.out.println(1/0);
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}
}
222

可以看到,运行时异常的时候finally还是执行了

//2.手动让程序结束System.exit(0);之前写算法的时候粗浅的用过这个函数暴力让程序停止
public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		t.fun();
	}
	public String fun() {
		try {
			System.out.println("111");
			System.exit(0);
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}
}
111

finally的内容并没有被执行,这里我们查一下System.exit()定义

    /**
     * Terminates the currently running Java Virtual Machine. The
     * argument serves as a status code; by convention, a nonzero status
     * code indicates abnormal termination.
     * <p>
     * This method calls the <code>exit</code> method in class
     * <code>Runtime</code>. This method never returns normally.
     * <p>
     * The call <code>System.exit(n)</code> is effectively equivalent to
     * the call:
     * <blockquote><pre>
     * Runtime.getRuntime().exit(n)
     * </pre></blockquote>
     *
     * @param      status   exit status.
     * @throws  SecurityException
     *        if a security manager exists and its <code>checkExit</code>
     *        method doesn't allow exit with the specified status.
     * @see        java.lang.Runtime#exit(int)
     */
    public static void exit(int status) {
        Runtime.getRuntime().exit(status);
    }
    /**

终止虚拟机,无论返回值是多少都会退出 0代表正常退出非0代表异常

System.exit(n)这个方法等价于Runtime.getRuntime().exit(n)
ps:这句好像是废话。。。。源码都有


虚拟机从那一句退出了,finally当然不会执行
//3.直接炸栈的运行时异常
在这里插入图片描述
出乎意料的是,就算炸栈了finally依然执行了
catch中return的实验

public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		System.out.println(t.fun());
	}
	public String fun() {
		try {
			System.out.println("111");
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}

}
111
222
return

public class Test {
	public static void main(String[] args) {
		Test t=new Test();
		System.out.println(t.fun());
	}
	public String fun() {
		try {
			System.out.println(1/0);
			return "return";
		} catch (Exception e) {
			return "catch";
		}finally{
			System.out.println("222");
		}
	}

}
222
catch

总结

  1. 程序错误(运行时异常,除0炸栈),try中return catch中return finally部分的代码都会执行
  2. 程序提前终止System.exit(0),会直接停止虚拟机,后面的finally语句不会被执行
  3. 执行是先return送值入栈,然后执行finally操作值,最后return返回结果
  4. 这个是常识性的,还是提一下,try catch相当于一个if 如果程序没异常就try里面的执行,如果有异常则执行catch里面的(返回值也是根据不同分支来的),finally只要没有中断程序就会执行到,再返回值

参考文章

https://blog.csdn.net/sinat_22594643/article/details/80509266
https://blog.csdn.net/baiyunshi/article/details/97375218
https://blog.csdn.net/weixin_41005006/article/details/80643681
https://blog.csdn.net/zoujian1993/article/details/45362931

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章