异常
异常就是程序生病了,不处理异常,程序就会翘翘,终止运行。对于异常的方法,一定要加上 @exception 文档注释。子类在重写父类中的抽象方法时处理的异常一定要比父类中处理的异常多,也就是说子类 throws 的异常一定只能比父类 throws 的异常少。说的更直白点,就是儿子一定不能比老子懒,再不济也要一样勤快。只要这样世界才会不断进步呢!
异常通常都是交给控制层来处理的,业务层和持久层专注于功能实现,在框架中甚至都不需要自行在控制层手工处理异常了。这是因为异常的设计之初就是为了帮助程序员方便处理错误,所以对待异常的原则应该是把出现问题和处理问题的地方分隔开来。业务层和持久层专注于处理业务和数据存储的问题,异常就全部抛给控制层去处理。
为什么要引入异常机制?
首先明确一个大前提,运行中的程序是一定无法避免出现问题的。比如断电了或者机器硬件坏了,文件找不到或者断网了。那怎么办呢? Java 就提供了异常机制来处理这类问题,通过异常机制来保证程序的健壮性和可维护性。
Java 提供的异常机制
问题分为 Error 和 Exception ,比如断电了或者机器硬件坏了,导致程序无法正常运行,这类属于 Error ,再厉害的程序员也不可能通过写代码解决。比如文件找不到或者断网了,导致程序无法正常运行,这类属于 Exception 。Exception 又分为编译异常和运行异常。编译异常通常给用户一个良好提示,运行异常在编写源代码的过程中要尽量解决掉。异常的命名应该是能够望文生义的,看到名字就能够猜出异常它的作用,无论是 java 标准类库还是自定义的异常类都应该遵循此规则,所有东西都应该是这样命名的啊。比如所有的输入输出异常都是 IOException 的子类!
java.lang.Throwable
throwable 是一个形容词,表示可抛出的意思。继承于 java.lang.Object 类,是 Error 和 Exception 的直接父类,它提供了异常类的基本功能。异常类继承层次结构图如下:
_Throwable.png既然所以异常类都是继承自 Throwable ,那么 Throwable 拥有的数据结构对于每一个异常类都是存在的。对于异常类需要知道的数据结构有以下几个点。
-
private String detailMessage
私有的 String 类型的属性,detailMessage 属性是用来描述异常的一段字符串信息。 - 至少两个构造函数,一个空构造函数,一个有参的构造函数来给私有属性 detailMessage 赋值。
-
public String getMessage()
方法,返回的就是异常类的 detailMessage 属性。 -
public String toString()
Throwable 重写了其父类 Object 的 toString() 方法,其输出格式为:异常类名:detailMessage
。 -
public void printStackTrace()
用来打印异常栈的信息,方便跟踪程序异常信息。控制台中打印异常栈信息的顺序是:先从异常抛出处的方法开始,一路回退到它最开始的调用方。为什么是这样子的呢?要明白,任何知识点都不是孤立的,他们之间一定能够建立联系。实现这个功能其实很简单啊,每一个线程都有一个方法调用栈,所以在遇到抛出异常方法的时候,在这个方法栈里面已经积压了很多方法了,边退栈边打印调用栈中方法的信息即可。理解了这一点,就理解了为什么会先从抛出异常方法处首先打印异常了,这不就是退栈的过程吗?此方法还有另外两个重载的方法,区别在于不同的参数,接收不同的输出流对象,如下public void printStackTrace(PrintWriter s)
和public void printStackTrace(PrintStream s)
方法
编译异常
5编译异常中最常见的就是 IOException 了,这里也只拿这类异常举例。编译异常必须显示的使用 try catch 进行预处理,否则编译阶段就不给通过。
这其实是 java 的一种设计思想,因为程序运行过程中发生资源不存在问题的可能性非常大,所以 JDK 类库设计者提供的某些方法或者构造方法就显示的使用 throws 关键字事先声明不处理某种异常,强迫调用这种方法的客户端程序员对这类异常进行预处理。
运行异常
运行异常是在程序运行过程中遇到不正常情况,由 JVM 创建并抛出的异常对象,如果没有对此异常进行处理的话,该异常对象一路被抛到 main() 方法中,JVM 就会自动调用该异常对象继承子 Throwable 的public void printStackTrace()
方法打印异常栈信息。这种运行时异常是无法在编译阶段检查出来的,因为它完全符合语法规则,只有在程序运行时,JVM 才能够判断它是否会出现问题。
比如NullPointerException
和ArithmeticException
就是常见的运行时异常类。运行时异常才是真正让人感觉到可怕的事情,在编写程序的过程中,即使语法上不要求进行异常处理,但是最好显示的去判断,去处理,程序编写可能显得比较麻烦,但是真正出问题了,就会发现一切付出都是值得的。
异常的处理流程
这里拿运行异常举例,编译异常是同理的。程序运行过程中,JVM 发现不正确情况,就在 new 出一个异常对象,并给它的 detailMessage 属性赋值。然后检查此处是否有 try catch 捕获了对应的异常对象,如果有则进入到 catch 代码段,如果没有则查看此方法是否使用 throws 关键字声明不处理异常,如果有则到调用此方法的方法中进行同样的流程处理。如果都没有,JVM 就会抛出此异常,程序被迫中断,控制台打印出相应的异常信息。
捕获异常的原则是必须要尽可能的细化,catch 代码块要呈金字塔铺开。做更细致化的异常处理是为了分化问题,便于对具体问题做具体分析和处理。要想成为一个好的程序员,一定要做到这些。
如果在主方法中使用 throws 声明不处理异常,这只是骗了编译器,语法上是通过了,但是主方法是 JVM 调用的,相当于还是抛给了 JVM ,该发生的异常还是会发生,程序该中断还是会中断。
throws 和 throw 以及 finally
throws 用在方法声明后面,后面跟的是一个或多个异常类,表示不处理异常。throw 用在方法内部,后面跟的是一个异常对象,表明此处抛出一个异常对象。如果一个方法中 throw 出一个异常对象,此方法就必须用 throws 声明不处理此异常,那就会抛给调用此方法的方法去处理。或者在此方法内部用 try catch 捕获,否则编译阶段都不会通过。
至于 finally 关键字,设计之初的本意是用来关闭资源的,比如输入输出流发生异常,在 finally 代码块中关闭资源。无论程序是否发生异常,finally 的代码都会被执行,这是 Java 的设计机制。
try 中发生异常,如果异常被 catch 捕获,则先执行catch 的语句,再执行 finally 的语句。如果异常没有被捕获,会先执行 finally 中的代码,再抛出此异常,因为如果先抛出了异常,那 finally 代码块的内容就无法执行了。
finally 简直太牛了,即使它前面有 return 语句,也会等到 fianlly 代码块的语句执行完了后再返回,但是如果 finally 有 return 语句,就会覆盖之前的语句,可以利用 return 返回栈的概念分析。实例代码如下:
public static int f() {
try {
System.out.println("try"+ 5/0);
return 1;
}catch(Exception e){
return 3;
}finally {
return 2;
}
}//此方法返回 2
如果 catch 捕获了相应异常,但是在处理异常的过程中又发生了异常,那么此时本应该抛出异常,但是会先执行 finally 代码块的内容后再抛出异常。但是如果 finally 代码块中有 return 语句,不仅会覆盖之前所有的 return 语句,还会是的程序就此结束。之前未来得及处理的异常就这样被隐藏了。
finally 代码块中不要出现 return 语句,不要乱写。上面讲到的内容只是有可能会遇到这样的面试题,只要知道 finally 中的 return 语句有一个 return 返回栈的概念就可以了,利用这个概念辅助分析。
自定义异常类
为什么要使用自定义异常类?Java 异常机制就是设计来处理程序中不正确问题的一种手段。这些都属于程序运行上的问题,计算机是用来解决问题的,在现实中有很多功能性错误,也就是程序运行上没有问题,但是在具体业务功能上不符合需求。比如银行系统中如果用户取钱大于账户余额,这就属于业务功能性错误。这类问题可以巧妙的利用异常机制来完成对她的处理。
如何自定义异常?自定义异常首先要做的当然是继承 java.lang.Exception 类,不然你要自己重新写一个吗?然后提供一个空构造函数和一个有参的构造函数,在其内部调用 super 关键字给父类的私有属性 detailMessage 属性赋值。重写 toString() 方法,输出格式为:类名:detailMessage
。根据具体需求还可以重写printStackTrace()
方法。