1 异常简介
程序运行时,发生的不被期望的事件,它阻止了程序按照预期的逻辑正常执行,这就是异常。
异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?
Java
提供了更加优秀的解决办法:异常处理机制。
异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java
中的异常可以是函数中的语句执行时引发的,也可以是通过 throw
语句手动抛出的,只要在Java
程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE
就会试图寻找异常处理程序来处理异常。
2 异常的分类
在
Java
语言中,Throwable
类是异常的顶层父类,所有的异常类都是它的子类。
2.1 异常结构图
Java
中的异常结构图如下:
Throwable
下层被分为Error
和Exception
两类。
2.2 Error
、Exception
(1)Error
类及其子类,代表了JVM
系统的内部错误和资源耗尽错误。
- 应用程序不应该抛出这种错误,因为一旦出现了这样的内部错误,除了通知用户,并尽力使程序安全地终止之外,别无他法。
Error
这种情况出现的很少。
(2)Exception
及其子类是异常处理的核心,需要重点关注。
Exception
又被划分为两个分支,一个分支派生于RuntimeEception
,另一个分支包含了其他异常。- 划分两个分支的规则是:由程序本身逻辑错误导致的异常属于
RuntimeException
;而程序逻辑本身没有问题,但由于像I/O
错误这类问题导致的异常属于其他异常。
派生于
RuntimeException
的异常包含下面几种情况:
错误的类型转换
数组访问越界
访问
null
指针不派生于
RuntimeException
的异常包括:
- 试图在文件尾部后面读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找
Class
对象,而这个字符串表示的类并不存在
2.3 非受查(unchecked)异常、受查(checked)异常
需要明确的是:受查、非受查是对于
javac
来说的,这样就很好理解和区分了。
(1)非受查异常(unchecked exception)
- 派生于
Error
类或者派生于RuntimeException
类的所有异常称为非受查异常。 javac
在编译的时候,不会提示和发现这样的异常。- 如果愿意,可以编写代码(如使用
try...catch...finally
)处理这样的异常,也可以不处理。
(2)受查异常(checked exception)
- 除了派生于
Error
类或者派生于RuntimeException
类的所有异常称为受查异常。 - 编译器将核查是否为所有的受查异常提供了异常处理器。
- 强制要求为这样的异常做预备处理工作(使用
try...catch...finally
或者throws
)。
注意:一个方法必须在方法签名上面通过
throws
声明所有可能抛出的受查异常,对于Error
和RuntimeException
异常不要通过throws
声明。
3 如何抛出异常
在程序中,我们通过使用throw new
来生成并抛出一个异常对象。
3.1 抛出Java中定义的异常
在Java
语言中已经定义了很多有用的标准异常类,我们在抛出这样的异常时候,可以:
-
找到一个合适的异常类
-
创建这个异常类的一个对象
-
将创建的对象抛出
注意:一旦方法抛出了异常,这个方法就不可能返回到调用它的位置。也就是说,不必为了返回的默认值或错误代码担忧。
例如,在我们编写一个读取文件内容的方法时候,还没读完文件就结束了,我们应该抛出EOFException
异常:
public String readData(Scanner in) throws EOFException {
...
while (...) {
if (!in.hasNext()) {
if (n < len) {
throw new EOFException();
}
}
...
}
return s;
}
3.2 抛出自定义异常
在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的异常问题。在这种情况下,我们可以创建自定义的异常类来描述。我们只需要:
- 定义一个派生于
Exception
的类,或者派生于Exception
子类的类。 - 在程序中创建并抛出自定义的这个类的实例。
例如,自定义一个FileFormatException
:
class FileFormatException extends IOException {
public FileFormatException() {}
public FileFormatException(String message) {
super(message);
}
}
现在,我们可以在程序中抛出自定义的异常了:
public String readData(Scanner in) throws EOFException {
...
while (...) {
if (!in.hasNext()) {
if (n < len) {
throw new FileFormatException("文件未读完,异常结束...");
}
}
...
}
return s;
}
按照国际惯例,自定义的异常应该总是包含如下的构造函数:
- 一个无参构造函数
- 一个带有
String
参数的构造函数,并传递给父类的构造函数- 一个带有
String
参数和Throwable
参数,并都传递给父类构造函数- 一个带有
Throwable
参数的构造函数,并传递给父类的构造函数
4 如何捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈信息。
4.1 使用try...catch...finally
语句块捕获异常
要想捕获异常,必须设置try/catch语句块。形式如下:
try {
// 处理业务逻辑...
} catch (SomeException e) {
// 处理异常
} finally {
// 无论如何都会执行的代码
}
说明:
-
如果在
try
语句块中的任何代码抛出了一个catch
语句块中说明的异常,那么:- 程序将跳过
try
语句块中剩余的代码 - 程序将会跳转到
catch
语句中执行这个异常的处理器代码
- 程序将跳过
-
如果在
try
语句块中的代码没有抛出任何异常,那么程序将会跳过所有catch
语句块。 -
如果
try
语句块中抛出了任何一个在catch
语句块中没有说明的异常,那么这个方法将会立刻退出。 -
finally
语句块是可选的,无论异常是否发生都会执行,通常用来做一些清理工作,比如流的关闭,数据库连接的关闭等。 -
当
catch
语句块处理完异常之后,程序不会回到抛出异常的那行代码,而是接着执行catch
语句块后边的程序代码。
4.2 使用throws
将异常传递给调用者
对于受查异常(checked exception),通常最好的方法就是什么也不做,将异常传递给方法的调用者,让调用者来操心如何处理。
例如,读取文件可能抛出IOException
异常,这是一个受查异常,可以通过在方法签名上使用throws
将异常传递给调用者,而不是在内部捕获:
public void read(String filename) throws IOException {
InputStream in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1) {
// 处理读取的内容...
}
}
4.3 捕获多个异常
在一个try/catch
语句块中可以捕获多个异常类型,并针对不同类型的异常做出不同的处理。可以按照下面的方式为每个异常类型使用一个单独的catch
子句:
try {
// 可能会抛出不同类型的异常
} catch (FileNotFoundException e) {
// 处理FileNotFoundException异常
} catch (UnknownHostException e) {
// 处理UnknownHostException异常
} catch (IOException e) {
// 处理IOException异常
}
如果多个异常的处理逻辑是一样的,那么可以合并在一个catch
语句中:
try {
// 可能会抛出不同类型的异常
} catch (FileNotFoundException | UnknownHostException e) {
// 处理FileNotFoundException与UnknownHostException异常
} catch (IOException e) {
// 处理IOException异常
}
捕获多个异常时,异常变量
e
隐含为final
变量,不可改变。
4.4 通过throw
再次抛出异常与异常链
在catch
语句中,还可以通过throw
再次抛出异常,这样做的目的是改变异常的类型。
(1)可以将原始异常包装,并抛出新的异常:
try {
// 连接数据库
} catch (SQLException e) {
Throwable se = new ServletException("database error.");
se.initCause(e);
throw se;
}
当捕获ServletException
的时候,就可以通过下边的语句获得原始异常信息:
Throwable s = se.getCause();
强烈建议使用这种包装技术,这样可以让用户抛出子系统中的高级异常,但又不会丢失原始异常细节。
(2)如果在一个方法中发生了受查异常(checked exception),而不允许抛出它,那么使用包装技术就十分合适。我们可以捕获这个异常,并将它包装成一个运行时异常。
(3)有时候我们可能只是想记录一下异常信息(打日志),也可以在catch
语句中捕获,打日志,然后再抛出这个异常:
try {
// 连接数据库
} catch (SQLException e) {
log.error("database error.", e);
throw e;
}
5 带资源的try
语句
对于以下的代码模式:
// 打开资源...
try {
// 处理业务逻辑
} finally {
// 关闭资源
}
Java SE 7
之后,只要资源实现了AutoCloseable
接口,就可以通过带资源的try
语句(try-with-resources
)简化代码编写:
try (Resource res = ...) {
// 处理业务逻辑...
}
当try
语句块推出的时候,会自动调用res.close()
方法。
下面给出一个典型的例子,读取文件中所有的单词:
try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8")) {
while (in.hasNext()) {
System.out.println(in.next());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
这块代码正常退出时,或者抛出异常时,最终都会调用in.close()
方法,就像使用了finally
语句块一样。
还可以一次指定多个资源,如下:
try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8");
PrintWriter out = new PrintWriter("out.txt")) {
while (in.hasNext()) {
out.println(in.next().toUpperCase());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
不论这个块如何退出,in
和out
都会被关闭。如果通过finally
语句块变成,那么就需要编写两个嵌套的try/finally
语句。
6 使用异常机制的一些技巧
-
异常处理不能代替简单的测试判断
例如,对于执行一个
退栈操作
来说,我们应该首先对栈进行判空处理:if (!stack.empty()) { stack.pop(); }
不能因为有异常机制,而省略掉这层判断。下面的代码不是一个好的写法:
try { stack.pop(); } catch (EmptyStackException e) { }
-
不要过分地细化异常
很多程序员喜欢将每一条语句封装在一个独立的
try/catch
语句中,例如:PrintStream out; Stack s; for (i = 0; i < 100; i++) { try { n = s.pop(); } catch (EmptyStackException e) { // 栈空 } try { out.writeInt(n); } catch (IOException e) { // 写文件发生错误 } }
对于这段代码逻辑来说,从栈中取出元素和写入流中,从业务上来说是一套操作,如果从栈中取元素发生了异常,那就没必要写流了,但是这样分开
try/catch
导致两部分操作不能互相感知。我们可以将它们合并到一个
try/catch
中,这样只要任何一步发生异常,就退出就好了:PrintStream out; Stack s; for (i = 0; i < 100; i++) { try { n = s.pop(); out.writeInt(n); } catch (EmptyStackException e) { // 栈空 } catch (IOException e) { // 写文件发生错误 } }
-
合理利用异常的层次结构
-
不要一味的只抛出
RuntimeException
异常。应该寻找更加适当的子类或者自定义异常。 -
不要只捕获
Throwable
异常,否则,程序代码更难读和维护。
-
-
子类重写带有
throws
声明的方法时,其throws
声明的异常必须在父类异常的可控范围内-
用于处理父类的
throws
方法的异常处理器,必须也适用于子类的这个带throws
方法,这是为了支持多态。 -
父类方法
throws
的是2
个异常,子类就不能throws
3
个及以上的异常。父类throws
IOException
,子类就必须throws
IOException
或者IOException
的子类。
-
-
多线程中,每个线程中的异常是相互独立的
-
Java
支持多线程编程。 -
每一个线程都是一个独立的执行流,独立的函数调用栈。
-
如果程序只有一个线程,那么没有被任何代码处理的异常会导致程序终止;如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。
-
Java
中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。
-
7 finally
块和return
注意:在
try
块中即便有return
、break
、continue
等改变执行流的语句,finally
块也会执行。
public static void main(String[] args) {
int re = bar();
System.out.println(re);
}
private static int bar() {
try{
return 5;
} finally {
System.out.println("finally语句块");
}
}
/* 输出:
finally语句块
5
*/
因此,可能会出现一下几个问题:
7.1 finally
中的return
会覆盖try
或者catch
中的return
返回值
public static void main(String[] args) {
int result;
result = foo();
System.out.println(result); // 2
result = bar();
System.out.println(result); // 4
}
private static int foo() {
try {
int a = 5 / 0;
} catch (Exception e){
return 1;
} finally {
return 2;
}
}
private static int bar() {
try {
return 3;
} finally {
return 4;
}
}
7.2 finally
中的return
会抑制(消灭)前面try
或者catch
块中的异常
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中的异常被抑制
private static int foo() {
try {
int a = 5 / 0;
return 1;
} catch (ArithmeticException amExp) {
throw new Exception("我将被忽略,因为下面的finally中使用了return");
} finally {
return 100;
}
}
// try中的异常被抑制
private static int bar() {
try {
int a = 5 / 0;
return 1;
} finally {
return 100;
}
}
7.3 finally
中的异常会覆盖(消灭)前面try
或者catch
中的异常
public static void main(String[] args) {
int result;
try {
result = foo();
System.out.println(result);
} catch (Exception e) {
System.out.println(e.getMessage()); // 输出:我是finaly中的Exception
}
try {
result = bar();
System.out.println(result);
} 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中的异常被抑制
private 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
中