目录
前言
在大学的时候没有设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的东西。它本身很简单,关系也很整洁:
一共只有Error
和Exception
两个直接继承类(那个StackRecorder是intellij从.class反编译过来的而且完全为空,毕竟不是java包里的,所以权当没看见吧= =!)
对于Error
和Exception
,原博客又下面一段话我觉得很不错:
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对异常处理的方法。
从这里可以看到,对于每一个异常,只要被捕获过了,下一次catch{...}
就不会再捕获了,所以throw e
并不会让它再被处理。
如果想要继续捕获这个异常,则需要throw
一个new
出来的Exception
。