Java异常解析

Java 将所有的错误封装为一个对象,其根本父类为 ThrowableThrowable有两个子类:ErrorException

Trowable 类中常用方法如下:

1
2
3
4
5
6
7
8
// 返回异常发生时的详细信息
public string getMessage();
// 返回异常发生时的简要描述
public string toString();
// 返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
public string getLocalizedMessage();
// 在控制台上打印Throwable对象封装的异常信息
public void printStackTrace();

Error

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.

ErrorThrowable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。大多数这样的错误都是异常条件。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类,因为大多数应用程序都不应该试图捕获它。在执行该方法期间,无需在其 throws 子句中声明可能抛出但是未能捕获的 Error 的任何子类,因为这些错误可能是再也不会发生的异常条件。

调用 stop() 方法时会抛出 java.lang.ThreadDeath 错误,但在通常的情况下,此错误不需要显式地捕捉。不过方法 stop() 已经被作废,因为如果强制让线程停止则有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的问题。

经典 Error 如下:

1
2
OutOfMemoryError
StackOverflowError

VirtulMachineError

有四种不同类型的 VirtulMachineError

  • OutOfMemoryError
  • StackOverflowError
  • InternalError
  • UnknownError

OutOfMemoryError

OutOfMemoryError 有八种不同类型:

  1. java.lang.OutOfMemoryError:Java 堆空间
  2. java.lang.OutOfMemoryError:GC 开销超过限制
  3. java.lang.OutOfMemoryError:请求的数组大小超过虚拟机限制
  4. java.lang.OutOfMemoryError:PermGen 空间
  5. java.lang.OutOfMemoryError:Metaspace
  6. java.lang.OutOfMemoryError:无法新建本机线程
  7. java.lang.OutOfMemoryError:杀死进程或子进程
  8. java.lang.OutOfMemoryError:发生 stack_trace_with_native_method

触发每种错误的原因各有不同。类似地,根据 OutOfMemoryError 不同的问题类型,对应的解决方案也不一样。

StackOverflowError

线程的堆栈存储了执行的方法、基本数据类型值、局部变量、对象指针和返回值信息,所有这些都会消耗内存。当栈深度超过虚拟机分配给线程的栈大小时,那么就会抛出 java.lang.StackOverflowError。通常由于执行程序中有一个错误,在线程重复递归调用同一个函数时会发生这个问题。

InternalError

JVM 抛出 java.lang.InternalError 有三个原因,虚拟机软件出现错误、系统软件底层出现错误或者硬件出现故障。

一般极少会遇到 InternalError 这样的错误。要了解哪些特定情况可能导致 InternalError,请在 Oracle 的 Java Bug 数据库 中搜索 InternalError。

UnknownError

当发生异常或错误,但 Java 虚拟机无法报告确切的异常或错误时,就会抛出 java.lang.UnknownError。UnknownError 很少出现。事实上,在 Oracle Java Bug 数据库 中搜索 UnknownError 时,只找到了2个 Bug。

Bug ID: JDK-4023606 AppletViewer generates java.lang.UnknownError when loading inner class.

Bug ID: JDK-4054295 UnknownError while loading class with super_class equal to zero

AWTError

AWT(Abstract Window Toolkit),中文译为抽象窗口工具包,是 Java 提供的用来建立和设置 Java 的图形用户界面的基本工具。AWTError一般也很少用到。事实上,在 Oracle Java Bug 数据库 中搜索 AWTError 时,只找到了8个 Bug。

Exception

Exception 类及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。Exception 分为未检查异常(RuntimeException)和已检查异常(非RuntimeException)。 未检查异常是因为程序员没有进行必需要的检查,因为疏忽和错误而引起的错误。几个经典的 RunTimeException 如下:

1
2
3
NullPointerException
ArithmaticException
ArrayIndexoutofBoundsException
  1. 可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。除了 Exception 中的 RuntimeException 及其子类以外,其他的 Exception 类及其子类(例如:IOException和ClassNotFoundException)都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
  2. 不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。RuntimeException表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。

RuntimeException

运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是 Java 编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,也会编译通过。

非RuntimeException

非运行时异常(也称受检查的异常)是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、SQLException 等以及用户自定义的 Exception 异常,一般情况下不自定义检查异常。

异常处理的机制

抛出异常

当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。

任何 Java 代码都可以抛出异常,如:自己编写的代码、来自 Java 开发环境包中代码,或者 Java 运行时系统。无论是谁,都可以通过 Java 的 throw 语句抛出异常。从方法中抛出的任何异常都必须使用throws子句。

捕获异常

在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

捕捉异常通过try-catch语句或者try-catch-finally语句实现。

异常的处理

1
2
3
throws //直接往上一层抛出异常;
try{}catch // 捕获异常
finally // 扫尾工作

throw 和 throws 两个关键字有什么不同

  • throw 是用来抛出任意异常的,你可以抛出任意 Throwable,包括自定义的异常类对象;
  • throws 总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常。如果方法抛出了异常,那么调用这个方法的时候就需要处理这个异常。

try-catch-finally-return执行顺序

  • 不管是否有异常产生,finally 块中代码都会执行;
  • 当 try 和 catch 中有 return 语句时,finally 块仍然会执行;
  • finally 是在 return 后面的表达式运算后执行的,所以函数返回值是在 finally 执行前确定的。无论 finally 中的代码怎么样,返回的值都不会改变,仍然是之前 return 语句中保存的值;
  • finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch 中保存的返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 按正常顺序执行。
try {
} catch() {
} finally {
return;
}

/**
* 1. 程序执行 try 块中 return 之前(包括 return 语句中的表达式运算)代码;
* 2. 再执行 finally 块,最后执行 try 中 return;
* 3. finally 块后面的 return 语句不再执行。
*/
try {
return;
} catch (){
} finally{
}
return;

/**
* 1.程序先执行 try,如果遇到异常执行 catch 块,
* 有异常则执行 catch 中 return 之前(包括 return 语句中的表达式运算)代码,再执行 finally 语句中全部代码,最后执行 catch 块中 return,finally 块后面的 return 语句不再执行。
* 无异常执行完 try 再 finally 再执行最后的 return 语句。
*/
try {
} catch() {
return;
} finally {
}
return;

/**
* 程序执行 try 块中 return 之前(包括 return 语句中的表达式运算)代码;
* 再执行 finally 块,因为 finally 块中有 return 所以提前退出。
*/
try {
return;
} catch() {
} finally {
return;
}

/**
* 程序执行catch块中return之前(包括return语句中的表达式运算)代码;
* 再执行finally块,因为finally块中有return所以提前退出。
*/
try {
} catch() {
return;
}finally {
return;
}

异常链

在设计模式中有一个叫做责任链模式,该模式是将多个对象链接成一条链,客户端的请求沿着这条链传递直到被接收、处理。同样Java异常机制也提供了这样一条链:异常链。

我们有两种方式处理异常,一是 throws 抛出交给上级处理,二是 try…catch 做具体处理。try…catch 的 catch 块我们可以不需要做任何处理,仅仅只用 throw 这个关键字将我们封装异常信息主动抛出来。然后在通过关键字 throws 继续抛出该方法异常。它的上层也可以做这样的处理,以此类推就会产生一条由异常构成的异常链。

通过使用异常链,我们可以提高代码的可理解性、系统的可维护性和友好性。

同理,我们有时候在捕获一个异常后抛出另一个异常信息,并且希望将原始的异常信息也保持起来,这个时候也需要使用异常链。在异常链的使用中,throw 抛出的是一个新的异常信息,这样势必会导致原有的异常信息丢失,如何保持?在 Throwable 及其子类中的构造器中都可以接受一个 Throwable cause 参数,该参数保存了原有的异常信息,通过 getCause() 就可以获取该原始异常信息。

1
2
3
4
5
6
7
public void test() throws XxxException{
try {
//do something:可能抛出异常信息的代码块
} catch (Exception e) {
throw new XxxException(e);
}
}

注意

精确原则

  1. 尽可能的减小 try 块——try 块不要包含太多的信息,仅包所需。
  2. catch 语句应当尽量指定具体的异常类型,不要一个Exception试图处理所有可能出现的异常

不要做渣男,负点责

  1. 既然捕获了异常,就要对它进行适当的处理。不要捕获异常之后又把它丢弃。
  2. 在异常处理模块中提供适量的错误原因信息,使其后续易于理解和阅读。
  3. 保证所有资源都被正确释放。 ——充分运用finally关键词。或者使用 Java 提供的语法糖 try() catch

两不要

  1. 不要在 finally 块中处理返回值。
  2. 不要在构造函数中抛出异常。

异常使用指南

应该在下列情况下使用异常(From 《Think in java》)。

  1. 在恰当的级别处理问题(在知道该如何处理异常的情况下才捕获异常)。
  2. 解决问题并且重新调用产生异常的方法。
  3. 进行少许修补,然后绕过异常发生的地方继续执行。
  4. 用别的数据进行计算,以代替方法预计会返回的值。
  5. 把当前运行环境下能做的事情尽量做完。然后把相同(不同)的异常重新抛到更高层。
  6. 终止程序。
  7. 进行简化。
  8. 让类库和程序更加安全。(这既是在为调试做短期投资,也是在为程序的健壮做长期投资)