【JAVA】准确理解try catch以及finally

前言

在大学的时候没有设java的课程,如今实习选择的是android岗位,java就成了每天必须打交道的语言。虽然有C++为基础,而且有一门名叫“面向对象程序设计”课介绍了面向对象语言的整体思路,想要正常使用倒是没什么问题,只是涉及到一些细节问题可能会有点理解不透彻,所以这里我试图利用博客的形式记录一下其中小细节的探究过程。

今天在使用异常捕获的时候,突然发现记不清当时在书上看到的try catch以及finally的逻辑了,应该是学习的不够透彻导致记得不清楚,这里要重新认真学习一下。
首先参考博客【Java学习笔记之三十三】详解Java中try,catch,finally的用法及分析这篇文章,进行一下导入。

一、 文前小测验

1. 一段简单的小代码

为了检验是否能够理解java的try/catch/finally机制,请阅读以下代码(不要作弊看答案或者是copy到本地运行哦),预测一下输出结果,看看是否正确。如果完全正确其实表示你已经理解try catch的逻辑了,不需要细读本文也可。代码如下:

public class TestException
{
    public TestException()
    {
    }

    boolean testEx() throws Exception
    {
        boolean ret = true;
        try
        {
            ret =testEx1();
        }
        catch (Exception e)
        {
            System.out.println("testEx,catch exception");
            ret =false;
            throw e;
        }
        finally
        {
            System.out.println("testEx,finally; return value=" + ret);
            return ret;
        }
    }

    boolean testEx1() throws Exception
    {
        boolean ret = true;
        try
        {
            ret =testEx2();
            if(!ret)
            {
                return false;
            }
            System.out.println("testEx1,at the end of try");
            return ret;
        }
        catch (Exception e)
        {
            System.out.println("testEx1,catch exception");
            ret =false;
            throw e;
        }
        finally
        {
            System.out.println("testEx1,finally; return value=" + ret);
            return ret;
        }
    }

    boolean testEx2() throws Exception
    {
        boolean ret = true;
        try
        {
            int b =12;
            int c;
            for(int i = 2; i >= -2; i--)
            {
                c= b / i;
                System.out.println("i="+ i);
            }
            return true;
        }
        catch (Exception e)
        {
            System.out.println("testEx2,catch exception");
            ret =false;
            throw e;
        }
        finally
        {
            System.out.println("testEx2,finally; return value=" + ret);
            return ret;
        }
    }

    public static void main(String[] args)
    {
        TestException testException1 =new TestException();
        try
        {
            testException1.testEx();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

2. 对代码的分析

我来简单整理一下这段代码。

  • 首先说一下TestException对象:
    它是一个测试异常捕获的类,构造函数可以忽略,其中有三个成员函数:testEx() testEx1() testEx2(),这三个函数返回值都是boolean类型,而且都有抛出异常。
    不过三个函数是顺次调用的,testEx()中调用了testEx1()testEx1()又调用了testEx2()
    因此异常的根源其实只有testEx2()中的2÷0那一段 (PS:这个因此毫无因果逻辑,只是我感觉语句通顺了一点,23333)
    这三个函数的一开始都声明了一个作为返回值的boolean变量,初始值为true。
    先说finally段,三个函数都是要输出那个boolean值并且返回它。
    再说catch段,都是将那个boolean值置为false并且抛出异常。
    最后说try段,try中的代码段三个函数各不相同,直接放在函数中讲解。

  • main()函数中new出了一个TestException对象,并且在try的代码块中调用了它的testEx()方法,并在后续的catch中对异常进行了捕获(这是testEx()的异常)。

  • testEx()方法中的try只是简单的调用了testEx1()函数并且把返回值赋给了那个boolean变量。所以它捕获的异常其实是testEx1()的异常

  • testEx1()的try是首先调用testEx2()函数,随后把返回值赋给了那个boolean变量。而后如果赋为了false,则直接return false,如果赋为了true,则输出一段日志随后return true;。所以这个函数也没有异常抛出,捕获的异常实际是testEx2()的异常

  • testEx2()的try是做一个for循环,并且将循环的标志int依次输出,只不过在第三次循环时会执行一条12÷0的操作语句从而抛出异常。循环结束后return true;

**好!**现在就已经完成了分析,那么你觉得运行Main()的结果是什么呢?我个人第一时间的想法是这样的:
由于testEx2()中对异常进行了捕获,所以除以0并不会让程序中断,还是会继续执行testEx1()
然后finally是必须执行的代码块,所以三个函数的finally的日志都会输出。
因此我预测的最后结果为

3. 给出答案

// 我猜测的输出结果
i=2
i=1
testEx2,catch exception
testEx2,finally; return value= false
testEx1,catch exception
testEx1,finally; return value= false
testEx,catch exception
testEx,finally; return value= false

4. 标准答案

那么揭晓答案!真正的输出结果是:

// 真正的输出结果
i=2
i=1
testEx2,catch exception
testEx2,finally; return value=false
testEx1,finally; return value=false
testEx,finally; return value=false

也就是说,异常捕获的catch代码段,其实只执行了一段。
如果你没有回答正确,那就请等我一起探究一下这其中的细节原理吧~

二、结果分析

1. try catch finally

老生常谈的无非就是,这三个代码块,其中try{...}这一块代码是需要被检测异常的代码;而catch{...}这一段是处理异常的代码;最后的finally{...}代码块是一定会被执行的代码。相信大家对于这样的说法已经不陌生了,所以下面我们具体看看,怎么检测异常,怎么处理异常,怎么叫一定会被执行呢?

首先研究一下我们的预计成果为什么和实际成果不一样?为什么只执行了一次catch()而且还是testEx2()中的该函数?
借用这篇博客中的讲解,我们可以了解到:

你可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部,写另一个try语句保护其他代码。每当遇到一个try语句,异常的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种异常进行处理,堆栈就会展开,直到遇到有处理这种异常的try语句。

也就是说,try和catch以及finally都是分离的(这不是废话吗= =!)然后你可以只写try不写catch,这样的话就是表示对该段代码进行检测异常,但是就算出现了错误,也只是记录一下有一个异常Exception,然后这个try{...}中的代码中断执行,继续向下。如果执行过程中发现有上一层的try,那么试图在这一层进行捕获。以此类推。(如果直到最后都没处理该怎么办呢……那就这么办呗,就不处理啦!反正只要出错是在try里就不会造成程序崩溃就是啦)

2. 实验-修改前面的测试题代码,使testEx1()中的catch{...}被执行

说了这么多抽象的,具体展示一下就是把上述代码中的testEx2()中的catch{}部分全部注释掉,你猜会咋样?
对啦!输出结果就是这么简单,依次出栈,输出上一层级的catch{...}里面的日志:

// 注释掉testEx2()中的catch部分后的输出结果
i=2
i=1
testEx2,finally; return value=true
testEx1,at the end of try
testEx1,finally; return value=true
testEx,finally; return value=true

稍微需要注意一下的也就是执行顺序,不过也很好理解。

3. 深入探究Exception以及Throwable

那么问题来了:观察代码发现三个函数每个的catch{...}部分都对捕获到的异常Exception e都有一句throw e的语句,为什么后面函数的catch{...}还是不能执行呢?
这涉及到了java中的异常类Exception,具体的那么我们一起来探究一下吧:

首先研究一下,从java源码可知Exception类继承自Throwable类:

public class Exception extends Throwable {...}

Throwable类又继承自Serializable

public class Throwable implements Serializable {...}

怎么样,这个继承关系很简单吧~那可得好好研究一下这个Throwable,到底是个什么东西。
顾名思义,按这个单次的意思肯定就是一个能够被Throw的东西。它本身很简单,关系也很整洁:
Throwable的继承关系
一共只有ErrorException两个直接继承类(那个StackRecorder是intellij从.class反编译过来的而且完全为空,毕竟不是java包里的,所以权当没看见吧= =!)
对于ErrorException,原博客又下面一段话我觉得很不错:

Error类对象(如动态连接错误等),由Java虚拟机生成并抛弃(通常,Java程序不对这类例外进行处理);
Exception类对象是Java程序处理或抛弃的对象。它有各种不同的子类分别对应于不同类型的例外。其中类RuntimeException代表运行时由Java虚拟机生成的例外,如算术运算例外ArithmeticException(由除0错等导致)、数组越界例外ArrayIndexOutOfBoundsException等;其它则为非运行时例外,如输入输出例外IOException等。Java编译器要求Java程序必须捕获或声明所有的非运行时例外,但对运行时例外可以不做处理。

这里如果深究的话可以研究很多有关Exception类型的内容,之后可以单独写一篇博客来研究。1

对于Throwable,源码中下面几个方法是比较常用的:

// Throwable.java
    public String getMessage() {
        return detailMessage;
    }

    public String getLocalizedMessage() {
        return getMessage();
    }

    public String toString() {
        String s = getClass().getName();
        String message = getLocalizedMessage();
        return (message != null) ? (s + ": " + message) : s;
    }
    
    public void printStackTrace() {
        printStackTrace(System.err);
    }
    
    public void printStackTrace(PrintStream s) {
        printStackTrace(new WrappedPrintStream(s));
    }

这里不用记得太清楚,源码还有很多常用的方法这里没有罗列出来。
这里只是为了说明:常用的这些方法大部分是在Throwable中实现的。看Exception的源码会发现,其中只是实现了5个构造函数:2

// Exception.java

//1
    public Exception() {
        super();
    }
//2
    public Exception(String message) {
        super(message);
    }
//3
    public Exception(String message, Throwable cause) {
        super(message, cause);
    }
//4
    public Exception(Throwable cause) {
        super(cause);
    }
// 5 todo:为什么这个是protected?
    protected Exception(String message, Throwable cause,
                        boolean enableSuppression,
                        boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

其中第五个构造函数为protected,具体原因还不清楚,后续我会另外写一篇博客进行研究。

这样,对于Exception类和Throwable的研究,姑且算是告一段落了。

4. 深入探究异常捕获流程

这里借用了博客Java 异常的捕获与处理详解 (一)中的一张图:
java异常处理流程

上面这张图讲述了java对异常处理的方法。
从这里可以看到,对于每一个异常,只要被捕获过了,下一次catch{...}就不会再捕获了,所以throw e并不会让它再被处理。
如果想要继续捕获这个异常,则需要throw一个new 出来的Exception


  1. //TODO: 研究Exception类型 ↩︎

  2. 为什么第五个构造函数是protected? ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值