JAVA 异常处理

一、异常分类

在JAVA中,所有异常都由 Throwable 继承而来;但在下一层立即分解为两个分支:ErrorException

Error

Error 类层次结构描述了JAVA运行时系统的内部错误和资源耗尽。应用程序不该抛出此种异常;如果出现了内部错误,除了告知用户和安全的退出,对于其他处理我们是无能为力的;比如出现 断电、硬盘损坏等等;但这种情况一般很少出现。

Exception

在我们进行JAVA开发时,我们需要关注的是 Exception 的层次结构,Exception大致分为两个分支:RuntimeException 和 非RuntimeException。划分两个分支的规则是:由程序设计错误导致的异常为 RuntimeException;而程序本身没问题,由像 I/O 错误这类造成的异常属于非 RuntimeException。

派生于RuntimeException的异常包含以下几种情况:

  • 错误的类型转换
  • 数组下标越界
  • 使用了null引用(空指针)

其他非RuntimeException异常也有几种常见情况:

  • 试图在文件尾部后读取数据
  • 试图打开不存在的文件
  • 通过字符串查找类,而类实际不存在(反射)

Java 异常层次图

hexo_java_exception.jpeg

从上面可以看出,”一旦出现 RuntimeException 那么一定是你的问题!” 这句话很有道理。JAVA语言规范将 派生于Error类或RuntimeException类的异常称之为 未检查异常(unchecked);其他异常称之为 已检查异常(checked);对于已检查,编译器将要求提供对应的异常处理器。

二、声明和抛出异常

异常的声明

在JAVA中如果方法内存在可能出现的异常,那么方法不光要告诉编译器返回什么,同时还要告诉编译器有可能产生哪些异常,如果文件的I/O异常等。

方法在其首部通过 throws 关键字声明该方法内可能抛出的已检查异常,如下:

1
2
3
public void testException throws FileNotFoundException(){
// 其他代码...
}

在继承关系中,子类声明的已检查异常不能比超类声明的已检查异常更加通用,简而言之就是子类声明的已检查异常范围不能大于父类声明的已检查异常范围,子类声明的已检查异常可以更精确。如果超类中某个方法未声明已检查异常,那么子类在重写该方法后同样不能声明任何已检查异常。

异常的抛出

在JAVA中抛出一个异常可以通过 throw 关键字进行抛出。但需要注意的是,我们抛出异常是要注意异常的作用场景,比如绝对不能抛出 Error 这种异常,抛出异常的样例如下:

1
2
3
4
public void testException throws FileNotFoundException(){
// 其他逻辑代码
throw new FileNotFoundException();
}

在书写程序时,可能遇到所有标准异常类型都无法描述我们的异常,此时我们可以自定义一个异常类,并继承相应的父异常类;比如一个文件操作异常,如果要自定义的话我们应该继承自 IOException;一般这种需要自定义异常的情况很少见。

三、捕获异常

try-catch 捕获异常

当调用的方法声明(抛出)了一个已检查异常时,我们有两种选择:

  • 1、继续向上(声明)抛出该已检查异常;
  • 2、捕获该已检查异常。

通过 try-catch 语句块捕获异常的样例如下

1
2
3
4
5
try{
// 调用声明已检查异常的方法相关代码
}catch(ExceptionType e){
// 对应出现异常的后续处理
}

try-catch 语句块的作用是:

  • 如果在 try 块中出现了在 catch 块中说明的异常(ExceptionType类型的异常),那么 try 块中余下代码将不会被执行,程序将执行 catch 块中的处理代码。
  • 如果在 try 块中没有出现异常,则程序正常执行,不会执行 catch 中的代码。
  • 如果 try 块中出现了未在 catch 块中说明的异常,则程序将异常一直向上抛出,直到jvm处理;一般为jvm爆出错误信息,然后程序终止。

捕获多个异常

try-catch 支持多异常的捕获,如下

1
2
3
4
5
6
7
try {
// 异常代码
} catch(FileNotFoundException e){
// 对应异常处理代码
} catch(EOFException e){
// 对应异常处理代码
} ...

**注意:在进行多异常捕获时,应尽量根据业务需要保证精确性;除非你知道你想要干什么,否则尽量不要直接捕获更高级的类似于 Exception 这种超类异常,因为这可能造成我们无法判定具体是 FileNotFoundException 还是 EOFException;对后续的处理会相当麻烦;除非真正的业务不关心具体细节;但还是那句话,异常捕获的界别取决于业务需要。 **

在 Java SE 7中,同一 catch 块可以捕获 彼此不存在子类关系 的多个异常进行统一处理,从而达到合并 catch 块,样例如下:

1
2
3
4
5
try {
// 异常代码
} catch(FileNotFoundException | EOFException e){
// 对应异常处理代码
} ...

在 catch 块中的异常变量 e 默认是 final 的;所以不能再 catch 块中为异常对象 e 重新赋值(改变引用指向)。

四、异常的重复抛出和异常连

实际上,在 catch 块中如初出现了新的异常或者有意的抛出新的异常,那么原本 try 块中的异常信息将被覆盖;这种情况一般一会出现,比如处理一些特定情况下的IO异常,样例如下:

1
2
3
4
5
6
try {
// 异常代码
} catch(FileNotFoundException e){
// 这里可能由于业务逻辑需要,我们并不关心具体是那种异常,我们只要知道是IO出了问题即可
throw new IOException();
}

当然这里由于新抛出了IO异常,所以原来的具体异常信息可能丢失,一般我们会采用以下两种方案:

  • 将原来的异常信息当做新异常的构造参数传入:throw new IOException("文件未找到: "+e.getMessage());
  • 也可以调用新异常的 newException.initCause(oldException) 完成异常信息的封装;后续可通过 newException.getCause(); 获得原始的异常信息。

五、finally 语句块

在异常处理机制中,允许使用 finally 语句块已完成一些必要的资源释放,比如关闭数据源、释放数据库连接等。

1
2
3
4
5
6
7
try {
// 异常代码
} catch(Exception e){
// 异常处理代码
} finally{
// 资源释放
}

注意:

  • finally 语句块中的代码总被执行!
  • 如果方法有返回值,finally 语句块中的代码总会在 try/catch 语句块中的 return 之前执行!
  • 如果 finally 语句块中也包含 return 返回值,那么这个值将作为最终返回值 覆盖掉 try/catch 中的 return 返回值!
  • 如果在 finally 语句块中执行相关操作(如资源释放)出现了异常,那么同 catch 块中一样,该异常将会覆盖原始异常!

六、Java SE 7 带资源释放的 try 语句

在 Java SE 7 中,如果资源类实现了 AutoCloseable 接口或其子接口(Closeable),那么可以使用带资源的 try-catch 块来释放资源,省去 finally 语句块,示例如下:

1
2
3
4
5
6
// 首先定义需要释放的资源
try (InputStream in = new FileInputStream("/testFile")){
// 异常代码
} catch(ExceptionType e){
// 异常处理代码
}

注意事项

  • 1、catch 块中需要捕获 资源释放中的 已检查已检查异常;也就是说如果 try 块后面的括号内的异常也必须在 catch 中体现(捕获);
  • 2、同原来的 catch/finally 中出现异常不一样,资源释放中出现异常将自动被抑制,原来的会重新抛出,不会覆盖原来的异常信息;资源释放产生的异常通过 addSuppressed 方法添加到原来异常的抑制列表中,可通过 getSuppressed 方法重新获取资源释放时产生的异常信息。

JAVA 异常处理
https://mritd.com/2016/02/05/java-exception/
作者
Kovacs
发布于
2016年2月5日
许可协议