本篇博客介绍Java中的异常机制及其基本使用。
什么是异常?
首先,我们再来回顾一下刚开始接触Java时犯的一些错误:
- 除0操作。
- 数组下标越界。
- 访问null对象。
上述几个出错程序运行之后打出的Exception开头的红色的提示就是异常信息。那什么是异常呢?
- 异常就是指程序运行时,出现错误时通知程序调用者的一种机制。
- 注意异常时运行时的一种机制,不是编译期。我们可以举个例子来理解一下什么是编译期的错误,比如我们写代码时出现关键字的拼写错误,此时出现的错误就是编译期的错误。
防御式编程
什么是防御式编程?
- 防御式编程时提高软件质量技术的有益辅助手段;
- 防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据;
- 这种思想将可能出现的错误造成的影响控制在有限的范围内。
错误在代码中是客观存在的。因此我们要让程序出现问题的时候及时通知程序猿。主要有两种方式:
- LBYL:Look Before You Leap,在操作之前就做充分的检查;
- EAFP:It’s Easier to Ask Forgiveness than Permission,先操作,遇到问题再处理。
异常的核心思想就是EAFP(先操作,遇到问题再处理)。
异常的优点
我们通过一个简单的例子来看一下网络通信LBYL和EAFP的处理流程:
- LBLY方式。
- EAFP方式。
对比两种风格的代码,我们可以发现:
- LBLY处理方式会将正常流程和错误处理流程代码混在一起,代码整体显得比较混乱;
- EAFP处理方式的正常流程和错误处理流程是分开的,更容易理解代码。
异常的基本用法
异常捕获
基本语法如下:
try {
// 可能出现异常的语句;
} [catch (异常类型 异常对象) {
// 异常的处理
} ...]
[finally {
// 异常出口
}]
- try代码块中放的是可能出现异常的代码;
- catch代码块中放的是出现异常后的处理行为;
- finally代码块中的代码用于处理善后工作,会在最后执行;
- 其中catch和finally都可以根据情况选择加或者不加。
下面我们看几种常见的异常处理方式:
- 不处理异常。
可以看到,如果对于异常不进行处理,程序就会在出现异常处终止,后序的代码将不再执行。其实这里异常并不是没有被处理,而是被JVM处理,JVM处理异常的方式就是打印出现异常的调用栈信息并终止程序。 - 使用try catch后的程序执行过程。
从运行结果可以看出,try中一旦有了异常,就会跳到对应的catch中,不再执行try中剩余的逻辑。 - catch只能处理对应种类的异常。
可以看到,这里的catch语句并没有捕获到数组访问越界的异常,该异常最终被JVM处理。 - catch可以有多个。
一段代码可能会抛出多种不同的异常,不同的异常有不同的处理方式。因此可以搭配多个catch代码块。如果多个异常的处理方式是完全相同的,也可以写成如下形式:
- 也可以使用一个catch捕获所有的异常。Exception类是所有异常类的父类。因此可以用这个类型表示捕获所有异常。catch进行匹配的时候,不仅可以捕捉到相同类型的异常,还可以捕捉到目标类型异常的子类对象。不推荐使用这种方式。
- finally表示最后的善后工作,如释放资源。
无论try中是否发生异常,finally中的代码一定会执行。 - 可以使用try回收资源。
和前一个代码的写法等价,将Scanner对象在try的()中创建,能够保证在try执行完毕后自动调用Scanner的close方法。 - 如果当前方法中没有合适的异常处理方式,异常就会沿着调用栈向上传递,直到最后交给JVM处理。
异常处理流程
- 程序先执行try中的代码;
- 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配;
- 如果找到匹配的异常类型,就会执行catch中的代码;
- 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者;
- 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行);
- 如果上层调用者也无法处理异常,异常就会继续向上传递;
- 一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止。
异常抛出
除了Java内置的类会抛出一些异常之外,程序猿也可以手动抛出某个异常。使用throw关键字来完成这个操作。
下面来看一个具体的例子:
异常说明
我们在处理异常时,通常希望知道这段代码中究竟会出现哪些可能的异常。我们可以使用throws关键字,把可能抛出的异常显式的标注在方法定义的位置。从而提醒调用者要注意捕获。
finally的注意事项
我们知道finally中的代码保证一定能够被执行到,而且是在return语句之前,有时会带来一些麻烦,我们看下面一个代码:
注意:
- finally执行的时机是在方法返回之前(try或者catch中如果有return会在这个return之前执行finally);
- 但是如果finally中也存在return语句,那么就会执行finally中的return,从而不会执行到try中原有的return;
- 不建议在finally中写return语句,编译器会有警告。
Java异常体系
- 顶层类Throwable派生出两个重要的子类,Error和Exception;
- 其中Error指的是Java运行时内部错误和资源耗尽错误。应用程序不抛出此类异常。这种内部错误一旦出现,除了告知用户并使程序终止之外,没有别的办法,这种情况很少出现;
- Exception是程序猿使用的异常类的父类;
- 其中Exception有一个子类称为RuntimeException,这里面又派生出很多我们常见的异常类NullPointerException等。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查异常,所有的其他异常称为受查异常。
如果一段代码可能抛出受查异常,那么必须显式进行处理。
从报错信息可以看出,我们必须对受查异常进行处理。
这里有两种处理方式:
- 方法一:使用try catch包裹起来进来:
- 方法二:在方法上加上异常说明,相当于将处理动作交给上级调用者。
自定义异常类
Java中虽然已经内置了丰富的异常类,但是我们实际场景中可能还有一些情况需要我们对异常类进行扩展,创建符合我们实际情况的异常。
这里,我们模拟一个用户登录的场景:
- 首先,我们自定义两个异常类UserException和PasswordException。
- 下面我们来写程序主逻辑。
- 运行效果如下:
注意:
- 自定义异常通常会继承自Exception或者RuntimeException;
- 继承自Exception的异常默认是受查异常;
- 继承自RuntimeException的异常默认是非受查异常;
- 自定义异常类往往不是创建一个类,而是创建一个系列。