前言
本章摘录自 Java 核心技术 卷1 基础知识,主要是异常的一些基础知识,以及一些使用的建议,重点部分已用红色字体标出。
正文
异常分类
所有的异常都是由 Throwable 继承而来,下一层分为 Error 和 Exception。
Error:Java 运行时系统的内部错误和资源耗尽错误。 例如,OutOfMemoryError 系统运行时内存不够,StackOverflowError 系统运行时堆栈溢出等严重情况。应用程序无法处理该类异常。
Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如,NullPointerException 空指针异常,FileNotFoundException 试图读取不存在的文件等异常。
Exception 下层又分为两个分支:一个分支派生于 RuntimeException;另一个分支包含其他异常。
派生于 RuntimeException 的异常:运行时异常。是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。
不是派生于 RuntimeException 的异常:编译时异常。是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。
划分两个分支的规则是:由程序错误导致的异常属于 RuntimeException;而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。
Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非受检异常,所有其他的异常称为受检异常,上图中也进行了说明。
声明受检异常
在如下两种情况时,需要声明受检异常:
- 调用一个抛出受査异常的方法,例如,FileInputStream 构造器。
- 程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常。
在方法的首部声明这个方法可能抛出的异常,如下所示:
public class CheckedExceptionDemo {
public Image loadImage(String s) throws IOException {
// 调用FileInputStream构造器加载文件流
}
}
如果一个方法有可能抛出多个受检异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。
public class CheckedExceptionDemo {
public Image loadImage(String s) throws FileNotFoundException, IOException {
// 调用FileInputStream构造器加载文件流
// 程序运行过程中,利用throw语句抛出IOException
if (...) {
throw new IOException("Invalid file path");
}
}
}
不需要声明非受检异常,比如派生于 Error 或者 RuntimeException 的异常。
public class CheckedExceptionDemo {
public void drawImage(int i) throws ArrayIndexOutOfBoundsException { // 不需要声明
}
}
另外,关于声明受检异常,我们还需要注意一点。如果当前类存在子类,且子类覆盖了父类的方法,子类方法中声明的受检异常不能比超类方法中声明的异常更通用。比如父类的方法声明了 IOException 异常,那子类的方法只能声明 IOException 异常或者 IOException 异常的子类(FileNotFoundException)或者不抛出异常,不能声明 Exception 异常。如果父类方法没有抛出任何受检异常,则子类也不能抛出任何受检异常。
自定义异常类
自定义一个异常类,比较简单,只需要定义一个派生于 Exception 的类,或者派生于 Exception 的子类的类。
一般我们需要考虑的是,定义一个派生于 RuntimeException 的异常类,还是定义一个派生于 Exception 的异常类?
这里再强调一下划分两者的规则是:由程序错误导致的异常属于 RuntimeException;而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。
一般,业务中定义的异常类都派生于 RuntimeException。
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = -7541127765936561867L;
public ServiceException() {
super();
}
public ServiceException(String message) {
super(message);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
}
捕获异常
设置try/catch
语句块,捕获代码中的异常。介绍几种try/catch
语句块的形式:
最简单的
try/catch
语句块。try {
// code
} catch (ExceptionType e) {
// handler for this type
}
- 如果在 try 语句块中的任何代码抛出了一个在 catch 子句中说明的异常类,那么程序会跳过 try 语句块的其余代码,执行 catch 子句中的处理器代码。
- 如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch 子句。
- 如果方法中的任何代码拋出了一个在 catch 子句中没有声明的异常类型,那么这个方法就会立刻退出。
捕获多个异常的
try/catch
语句块。try {
// code that might throw exceptions
} catch (FileNotFoundException e) {
// emergency action for missing files
} catch (UnknownHostException e) {
// emergency action for unknown hosts
} catch (IOException e) {
// emergency action for all other I/O problems
}
同一个 catch 子句捕获多个异常(Java SE 7+)。
try {
// code that might throw exceptions
} catch (FileNotFoundException | UnknownHostException e) {
// emergency action for missing files and unknown hosts
} catch (IOException e) {
// emergency action for all other I/O problems
}
捕获多个异常时,异常变量(e)隐含为 final 变量,不能在 catch 子句体中为 e 赋不同的值。
finally 子句,用来关闭申请的资源。
InputStream in = new FileInputStream(...);
try {
// 1
// code that might throw exceptions
// 2
} catch (IOException e) {
// 3
// show error message
// 4
} finally {
// 5
in.close();
}
// 6
- 代码没有抛出异常。在这种情况下,程序首先执行 try 语句块中的全部代码,然后执行 finally 子句中的代码。随后,继续执行 try 语句块之后的代码。执行顺序 1、2、5、6。
- try 语句块抛出一个 IOException 异常。在这种情况下,程序跳过 try 语句块中的剩余代码,转去执行 catch 子句中的代码,然后执行 finally 子句中的代码。如果 catch 子句没有抛出异常,程序将继续执行 try 语句块之后的代码。执行顺序 1、3、4、5、6。如果 catch 子句也抛出了一个异常,异常将被抛回这个方法的调用者。执行顺序 1、3、5。
- try 语句块抛出了一个异常,但这个异常不是由 catch 子句捕获的。在这种情况下,程序跳过 try 语句块中的剩余代码,然后执行 finally 子句中的代码,并将异常抛给这个方法的调用者。执行顺序 1、5。
try 语句可以只有 finally 子句,而没有 catch 子句。
InputStream in = ...;
try {
// code that might throw exceptions
} finally {
in.close();
}
无论在 try 语句块中是否遇到异常,finally 子句中的 in.close() 语句都会被执行。当然,如果 try 语句块抛出了一个异常,因为这里并没有使用 catch 子句处理,所以这个异常会继续往上抛出。
解搞合 try/catch 和 try/finally 语句块。
InputStream in = ...;
try {
try {
// code that might throw exceptions
} finally {
in.close();
}
} catch (IOException e) {
// show error message
}
在 Java 核心技术 卷1 中提供了以上有意思的写法。内层的 try 语句块只有一个职责,就是确保关闭输入流。外层的 try 语句块也只有一个职责,就是捕获可能出现的异常。 这种设计方式不仅清楚,而且还有一个功能,就是可以同时捕获 finally 子句中出现的异常。
finally 子句中不能包含 return 语句。
当 finally 子句包含 return 语句时,将会出现一种意想不到的结果,假设利用 return 语句从 try 语句块中退出。在方法返回前,finally 子句的内容将被执行。如果 finally 子句中也有一个 return 语句,这个返回值将会覆盖原始的返回值。
public static int f(int n) {
try {
int r = n * n;
return r;
} finally {
if (n == 2) return 0;
}
}
如果调用 f(2),那么 try 语句块的计算结果为 r = 4,并执行 return 语句然而,在方法真正返回前,还要执行 finally 子句。finally 子句将使得方法返回 0,这个返回值覆盖了原始的返回值 4。
- 带资源的 try 语句(Java SE 7+)。
假设在 try 语句块中的代码会抛出一个异常,执行 finally 语句块,close 方法也抛出一个异常,则 try 语句块中的异常会被丢弃,转而抛出 close 方法的异常。如果需要处理这种情况,只抛出 try 语句块中的异常,代码会变得比较繁琐。
InputStream in = ...;
Exception ex = null;
try {
// code that might throw exceptions
} catch (Exception e) {
ex = e;
throw e;
} finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e) {
if (ex == null) {
throw e;
}
}
}
Java SE 7 中推出了新的特性,支持带资源的 try 语句(try-with-resources),来代替 finally 子句关闭资源。假设当前资源属于一个实现了 AutoCloseable 接口的类,使用以下带资源的 try 语句可以实现自动关闭资源。
try (Resource res = ...) {
work with res
}
try 块退出时,会自动调用 res.close() 方法。
带资源的 try 语句支持指定一个或多个资源:
try (Scanner in = new Scanner(new FileInputStream("7usr/share/dict/words"), "UTF-8")) {
// code
} catch (FileNotFoundException e) {
// handler for this type
}
try (Scanner in = new Scanner(new FileInputStream("7usr/share/dict/words"), "UTF-8");
PrintWriter out = new PrintWriter("out.txt")) {
// code
} catch (FileNotFoundException e) {
// handler for this type
}
这个块正常退出时,或者存在异常时,in 和 out 都会关闭,就好像使用了 finally 块一样。
如果 try 块抛出一个异常,而且 close 方法也抛出一个异常。带资源的 try 语句可以很好地处理这种情况。原来的异常会重新抛出,而 close 方法抛出的异常会“被抑制”。这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。如果对这些异常感兴趣,可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表。
所以,涉及到需要关闭资源的操作,建议都使用带资源的 try 语句(try-with-resources)。
再次抛出异常与异常链
在 catch 子句中再次抛出异常,建议将原始异常设置为新异常的“原因”。
public class ExceptionChainDemo {
public void fun1() throws FileNotFoundException {
throw new FileNotFoundException("fun1");
}
public void fun2() throws IOException {
try {
fun1();
} catch (FileNotFoundException e) {
throw new IOException("fun2", e);
}
}
public static void main(String[] args) throws Exception {
ExceptionChainDemo test2 = new ExceptionChainDemo();
test2.fun2();
}
}
运行代码,输出:
Exception in thread "main" java.io.IOException: fun2
at test7.ExceptionChainDemo.fun2(ExceptionChainDemo.java:22)
at test7.ExceptionChainDemo.main(ExceptionChainDemo.java:28)
Caused by: java.io.FileNotFoundException: fun1
at test7.ExceptionChainDemo.fun1(ExceptionChainDemo.java:15)
at test7.ExceptionChainDemo.fun2(ExceptionChainDemo.java:20)
... 1 more
因为在 catch 子句中再次抛出异常使用了如下构造器,将 fun1 方法中抛出的异常设置为新异常的“原因”,所以控制台在打印异常栈的时候,可以追踪到整个异常链。
public IOException(String message, Throwable cause) {
super(message, cause);
}
如果 IOException 中没有提供这样的构造器,可以使用 Throwable 的 initCause 方法,初始化新异常的“原因”。
public void fun2() throws IOException {
try {
fun1();
} catch (FileNotFoundException e) {
IOException e2 = new IOException("fun2");
e2.initCause(e);
throw e2;
}
}
执行结果和上面的代码一样。
如果 catch 子句中没有初始化新异常的“原因”,则不会打印 fun1 方法中抛出的异常。
public void fun2() throws IOException {
try {
fun1();
} catch (FileNotFoundException e) {
throw new IOException("fun2");
}
}
运行代码,输出:
Exception in thread "main" java.io.IOException: fun2
at test7.ExceptionChainDemo.fun2(ExceptionChainDemo.java:22)
at test7.ExceptionChainDemo.main(ExceptionChainDemo.java:28)
转载
- Java 核心技术 卷1 基础知识 第10版
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/qsqxdg 来源:殷建卫 - Java 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。