Java 异常的简介
Java 异常是 Java 提供的一种识别及响应错误的一致性机制. 具体来说, 异常机制提供了程序退出的安全通道. 当出现错误后, 程序执行的流程发生改变, 程序的控制权转移到异常处理器. Java 异常机制可以使程序中异常处理代码和正常业务代码分离, 保证程序代码更加优雅, 并提高程序健壮性.
Java 中的异常可以是函数中的语句执行时引发的, 也可以是程序员通过 throw 语句手动抛出的, 只要在 Java 程序中产生了异常, 就会用一个对应类型的异常对象来封装异常, JRE 就会试图寻找异常处理程序来处理异常.
Java 异常的体系结构
Throwable 类是 Java 异常类型的顶层父类, 一个对象只有是 Throwable 类的 (直接或者间接) 实例, 他才是一个异常对象, 才能被异常处理机制识别.
Throwable 包含两个子类: Error 和 Exception. 它们通常用于指示发生了异常情况.
Throwable 包含了其线程创建时线程执行堆栈的快照, 它提供了 printStackTrace()等接口用于获取堆栈跟踪数据等信息.
Exception 及其子类是 Throwable 的一种形式, 它指出了合理的应用程序想要捕获的条件.
RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类. RuntimeException 及其子类都被称为运行时异常.
编译器不会检查 RuntimeException 异常. 如果代码会产生 RuntimeException 异常, 则需要通过修改代码进行避免.
如, 除数为零时, 抛出 ArithmeticException 异常. RuntimeException 是 ArithmeticException 的超类. 当代码发生除数为零的情况时, 倘若既没有通过 throws 声明抛出 ArithmeticException 异常, 也没有通过 try...catch... 处理该异常, 也能通过编译. 这就是 "编译器不会检查 RuntimeException 异常"!
- public class Test {
- public static void main(String[] args) {
- int num = 10;
- System.out.println(num/0);
- }
- }
如上代码, 程序并未报错, 运行结果出错: Exception in thread "main" java.lang.ArithmeticException: / by zero
Error 和 Exception 一样, Error 也是 Throwable 的子类. 它用于指示合理的应用程序不应该试图捕获的严重问题, 大多数这样的错误都是异常条件. 和 RuntimeException 一样, 编译器也不会检查 Error.
根据 Javac 对异常的处理要求, 将异常类分为 2 类.
检查异常(checked exception): 除了 Error 和 RuntimeException 的其它异常. javac 强制要求程序员为这样的异常做预备处理工作(使用 try...catch...finally 或者 throws). 在方法中要么用 try-catch 语句捕获它并处理, 要么用 throws 子句声明抛出它, 否则编译不会通过. 这样的异常一般是由程序的运行环境导致的. 因为程序可能被运行在各种未知的环境下, 而程序员无法干预用户如何使用他编写的程序, 于是程序员就应该为这样的异常时刻准备着. 如 SQLException , IOException,ClassNotFoundException 等.
非检查异常 (unckecked exception):Error 和 RuntimeException 以及他们的子类. javac 在编译时, 不会提示和发现这样的异常, 不要求在程序处理这些异常. 所以如果愿意, 我们可以编写代码处理(使用 try...catch...finally) 这样的异常, 也可以不处理. 对于这些异常, 我们应该修正代码, 而不是去通过异常处理器处理 . 这样的异常发生的原因多半是代码写的有问题. 如除 0 错误 ArithmeticException, 错误的强制类型转换错误 ClassCastException, 数组索引越界 ArrayIndexOutOfBoundsException, 使用了空对象 NullPointerException 等等.
需要明确的是: 检查和非检查是对于 javac 来说的.
Java 异常的处理
基本异常处理
在处理异常时, 对于检查异常, 有 2 种不同的处理方式: 使用 try...catch...finally 语句块处理. 或, 函数签名中使用 throws 声明交给函数调用者 caller 去解决.
首先对 Java 异常机制用到的几个关键字进行区分: try,catch,finally,throw,throws.
try -- 用于监听. 将要被监听的代码 (可能抛出异常的代码) 放在 try 语句块之内, 当 try 语句块内发生异常时, 异常就被抛出.
catch -- 用于捕获异常. catch 用来捕获 try 语句块中发生的异常.
finally -- finally 语句块总是会被执行. 它主要用于回收在 try 块里打开的物力资源(如数据库连接, 网络连接和磁盘文件). 只有 finally 块执行完成之后, 才会回来执行 try 或者 catch 块中的 return 或者 throw 语句, 如果 finally 中使用了 return 或者 throw 等终止方法的语句, 则就不会跳回执行, 直接停止.
throw -- 用于抛出异常.
throws -- 用在方法签名中, 用于声明该方法可能抛出的异常.
try...catch...finally 语句块
- try{
- //try 块中放可能发生异常的代码.
- // 如果执行完 try 且不发生异常, 则接着去执行 finally 块和 finally 后面的代码(如果有的话).
- // 如果发生异常, 则尝试去匹配 catch 块.
- }catch(SQLException SQLexception){
- // 每一个 catch 块用于捕获并处理一个特定的异常, 或者这异常类型的子类. Java7 中可以将多个异常声明在一个 catch 中.
- //catch 后面的括号定义了异常类型和异常参数. 如果异常与之匹配且是最先匹配到的, 则虚拟机将使用这个 catch 块来处理异常.
- // 在 catch 块中可以使用这个块的异常参数来获取异常的相关信息. 异常参数是这个 catch 块中的局部变量, 其它块不能访问.
- // 如果当前 try 块中发生的异常在后续的所有 catch 中都没捕获到, 则先去执行 finally, 然后到这个函数的外部 caller 中去匹配异常处理器.
- // 如果 try 中没有发生异常, 则所有的 catch 块将被忽略.
- }catch(Exception exception){
- //...
- }finally{
- //finally 块通常是可选的.
- // 无论异常是否发生, 异常是否匹配被处理, finally 都会执行.
- // 一个 try 至少要有一个 catch 块, 否则, 至少要有 1 个 finally 块. 但是 finally 不是用来处理异常的, finally 不会捕获异常.
- //finally 主要做一些清理工作, 如流的关闭, 数据库连接的关闭等.
- }
需要注意的地方
1,try 块中的局部变量和 catch 块中的局部变量(包括异常变量), 以及 finally 中的局部变量, 他们之间不可共享使用.
2, 每一个 catch 块用于处理一个异常. 异常匹配是按照 catch 块的顺序从上往下寻找的, 只有第一个匹配的 catch 会得到执行. 匹配时, 不仅运行精确匹配, 也支持父类匹配, 因此, 如果同一个 try 块下的多个 catch 异常类型有父子关系, 应该将子类异常放在前面, 父类异常放在后面, 这样保证每个 catch 块都有存在的意义.
3,java 中, 异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去. 也就是说: 当一个函数的某条语句发生异常时, 这条语句的后面的语句不会再执行, 它失去了焦点. 执行流跳转到最近的匹配的异常处理 catch 代码块去执行, 异常被处理完后, 执行流会接着在 "处理了这个异常的 catch 代码块" 后面接着执行.
有的编程语言当异常被处理后, 控制流会恢复到异常抛出点接着执行, 这种策略叫做: resumption model of exception handling(恢复式异常处理模式 )
而 Java 则是让执行流恢复到处理了异常的 catch 块后接着执行, 这种策略叫做: termination model of exception handling(终结式异常处理模式)
- public class Test {
- public static void main(String[] args) {
- try {
- div();
- } catch (ArithmeticException e) {
- System.err.println("异常处理");
- } finally {
- System.out.println("处理完成");
- }
- }
- public static void div() {
- int num = 10/0;
- System.err.println("我是测试语句");
- }
- }
throws 函数声明
throws 声明: 如果一个方法内部的代码会抛出检查异常(checked exception), 而方法自己又没有完全处理掉, 则 javac 保证你必须在方法的签名上使用 throws 关键字声明这些可能抛出的异常, 否则编译不通过.
throws 是另一种处理异常的方式, 它不同于 try...catch...finally,throws 仅仅是将函数中可能出现的异常向调用者声明, 而自己则不具体处理.
采取这种异常处理的原因可能是: 方法本身不知道如何处理这样的异常, 或者说让调用者处理更好, 调用者需要为可能发生的异常负责.
- public void div() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN {
- // 内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常, 或者他们的子类的异常对象.
- }
finally 块
finally 块不管异常是否发生, 只要对应的 try 执行了, 则它一定也执行. 只有一种方法让 finally 块不执行: System.exit(). 因此 finally 块通常用来做资源释放操作: 关闭文件, 关闭数据库连接等等.
良好的编程习惯是: 在 try 块中打开资源, 在 finally 块中清理释放这些资源.
需要注意的地方:
1,finally 块没有处理异常的能力. 处理异常的只能是 catch 块.
2, 在同一 try...catch...finally 块中 , 如果 try 中抛出异常, 且有匹配的 catch 块, 则先执行 catch 块, 再执行 finally 块. 如果没有 catch 块匹配, 则先执行 finally, 然后去外面的调用者中寻找合适的 catch 块.
3, 在同一 try...catch...finally 块中 ,try 发生异常, 且匹配的 catch 块中处理异常时也抛出异常, 那么后面的 finally 也会执行: 首先执行 finally 块, 然后去外围调用者中寻找合适的 catch 块.
此外, 也有特殊情况: 在 try 块中即便有 return,break,continue 等改变执行流的语句, finally 也会执行.
- public class Test {
- public static void main(String[] args) {
- int num = div();
- System.out.println(num);
- }
- public static int div() {
- try {
- int num = 10/0;
- } catch (ArithmeticException e) {
- return 5;
- } finally {
- return 4;
- }
- }
- }
try...catch...finally 中的 return 只要能执行, 就都执行了, 他们共同向同一个内存地址 (假设地址是 0x80) 写入返回值, 后执行的将覆盖先执行的数据, 而真正被调用者取的返回值就是最后一次写入的.
finally 中的 return 会覆盖 try 或者 catch 中的返回值. 见上一个代码示例:
finally 中的 return 会抑制 (消灭) 前面 try 或者 catch 块中的异常, 示例代码如下:
- @SuppressWarnings("all")
- public class Test {
- public static void main(String[] args) {
- int result;
- try {
- result = foo();
- System.out.println(result); // 输出 100
- } catch (Exception e){
- System.out.println(e.getMessage()); // 没有捕获到异常
- }
- try{
- result = bar();
- System.out.println(result); // 输出 100
- } catch (Exception e){
- System.out.println(e.getMessage()); // 没有捕获到异常
- }
- }
- //catch 中的异常被抑制
- public static int foo() {
- try {
- int a = 5/0;
- return 1;
- }catch(ArithmeticException amExp) {
- throw new Exception("我将被忽略, 因为下面的 finally 中使用了 return");
- }finally {
- return 100;
- }
- }
- //try 中的异常被抑制
- public static int bar() {
- try {
- int a = 5/0;
- return 1;
- }finally {
- return 100;
- }
- }
- }
finally 中的异常会覆盖 (消灭) 前面 try 或者 catch 中的异常
- @SuppressWarnings("all")
- public class Test {
- public static void main(String[] args) {
- int result;
- try{
- result = foo();
- } catch (Exception e){
- System.out.println(e.getMessage()); // 输出: 我是 finaly 中的 Exception
- }
- try{
- result = bar();
- } catch (Exception e){
- System.out.println(e.getMessage()); // 输出: 我是 finaly 中的 Exception
- }
- }
- //catch 中的异常被抑制
- public static int foo() throws Exception {
- try {
- int a = 5/0;
- return 1;
- }catch(ArithmeticException amExp) {
- throw new Exception("我将被忽略, 因为下面的 finally 中抛出了新的异常");
- }finally {
- throw new Exception("我是 finaly 中的 Exception");
- }
- }
- //try 中的异常被抑制
- public static int bar() throws Exception {
- try {
- int a = 5/0;
- return 1;
- }finally {
- throw new Exception("我是 finaly 中的 Exception");
- }
- }
- }
这些例子展示了一些不常见的编码方式, 从而引发了各种奇怪的问题, 因而在编码中做到:
不要在 fianlly 中使用 return.
不要在 finally 中抛出异常.
减轻 finally 的任务, 不要在 finally 中做一些其它的事情, finally 块仅仅用来释放资源是最合适的.
将尽量将所有的 return 写在函数的最后面, 而不是 try ... catch ... finally 中.
异常的链化
在一些大型的, 模块化的软件开发中, 一旦一个地方发生异常, 则如骨牌效应一样, 将导致一连串的异常. 假设 B 模块完成自己的逻辑需要调用 A 模块的方法, 如果 A 模块发生异常, 则 B 也将不能完成而发生异常, 但是 B 在抛出异常时, 会将 A 的异常信息掩盖掉, 这将使得异常的根源信息丢失. 异常的链化可以将多个模块的异常串联起来, 使得异常信息不会丢失.
异常链化: 以一个异常对象为参数构造新的异常对象. 新的异对象将包含先前异常的信息. 这项技术主要是异常类的一个带 Throwable 参数的函数来实现的. 这个当做参数的异常, 我们叫他根源异常(cause). 异常的链化可以将多个模块的异常串联起来, 使得异常信息不会丢失.
自定义异常
如果要自定义异常类, 则扩展 Exception 类即可, 因此这样的自定义异常都属于检查异常(checked exception). 如果要自定义非检查异常, 则扩展自 RuntimeException.
按照惯例, 自定义的异常应该总是包含如下的构造函数:
一个无参构造函数
一个带有 String 参数的构造函数, 并传递给父类的构造函数.
一个带有 String 参数和 Throwable 参数, 并都传递给父类构造函数
一个带有 Throwable 参数的构造函数, 并传递给父类的构造函数.
异常注意事项
1, 当子类重写父类的带有 throws 声明的函数时, 其 throws 声明的异常必须在父类异常的可控范围内 -- 用于处理父类的 throws 方法的异常处理器, 必须也适用于子类的这个带 throws 方法 . 这是为了支持多态.
2,Java 程序可以是多线程的. 每一个线程都是一个独立的执行流, 独立的函数调用栈. 如果程序只有一个线程, 那么没有被任何代码处理的异常 会导致程序终止. 如果是多线程的, 那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束.
本文参考了:
本文大部分出自 https://www.cnblogs.com/lulipro/p/7504267.html
https://www.cnblogs.com/skywang12345/p/3544168.html
来源: http://www.bubuko.com/infodetail-2876691.html