通过异常处理错误

异常允许我们(如果没有其他手段)强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到稳定状态。

new 在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用,所有标准异常类都有两个构造器:一个是默认构造器,另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:

1
throw new NullPointerException("t = null");

此外,能够抛出任意类型的 Throwable 对象,它是异常类型的根类。

创建自定义异常

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
53
54
55
class AException extends Exception {

public AException() {
}

public AException(String message) {
super(message);
}

public static void f() throws AException {

throw new AException();
}

public static void g() throws AException {

throw new AException("g()");
}
}

try {
AException.f();
} catch (AException e) {
e.printStackTrace(System.out);
}

// 输出
/*
AException
at AException.f(Main.java:39)
at Main.main(Main.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
*/


try {
AException.g();
} catch (AException e) {
e.printStackTrace();
}

// 输出
/*
AException: g()
at AException.g(Main.java:38)
at Main.main(Main.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
*/

printStackTrace() 方法将打印:从方法调用处直到异常抛出处的方法调用序列,默认是将信息输出到标准错误流。
printStackTrace(System.out) 代表将信息发送到 System.out

异常与记录日志

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
class AException extends Exception {

// Logger.getLogger() 方法创建了一个 String 参数相关联的 Logger 对象
// 这个 Logger 对象会将其输出发送到 System.err ,即标准错误流
private static Logger logger = Logger.getLogger("AException");

public static void logException(Exception e) {

// 将 printStackTrace 的信息发送到 stringWriter 中
// logger.severe 代表一种消息级别
StringWriter stringWriter = new StringWriter();
e.printStackTrace(new PrintWriter(stringWriter));
logger.severe(stringWriter.toString());
}
}

try {
throw new NullPointerException();
} catch (NullPointerException e) {
AException.logException(e);
}

// 输出
/*
二月 05, 2017 3:36:42 下午 AException logException
严重: java.lang.NullPointerException
at Main.main(Main.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
*/

栈轨迹

printStackTrace() 方法所提供的信息可以通过 getStackTrace() 方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。数组中的第一个元素是栈顶元素,并且是调用序列中最后一个方法调用。数组中最后一个元素是栈底元素,是调用序列中第一个方法调用。

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
class A {

static void f() {

try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
System.out.println(stackTraceElement.getMethodName());
}
}
}

static void g() {
f();
}
}

A.f();

// 输出:
/*
f
main
invoke0
invoke
invoke
invoke
main
*/


A.g();

// 输出:
/*
f
g
main
invoke0
invoke
invoke
invoke
main
*/

重新抛出异常

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。此外,异常对象的信息将得以保存,除非使用 fillInStackTrace() 更新异常信息。

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
class A {

static void f() throws Exception {
throw new Exception();
}

static void g() throws Exception {
try {
f();
} catch (Exception e) {
throw e;
}
}

static void k() throws Exception {
try {
f();
} catch (Exception e) {
throw (Exception) e.fillInStackTrace();
}
}
}

try {
A.g();
} catch (Exception e) {
e.printStackTrace();
}

// 输出:
/*
java.lang.Exception
at A.f(Main.java:26)
at A.g(Main.java:31)
at Main.main(Main.java:14)
... 5 more
*/


try {
A.k();
} catch (Exception e) {
e.printStackTrace();
}

// 输出:
/*
java.lang.Exception
at A.k(Main.java:41)
at Main.main(Main.java:14)
... 5 more
*/

异常链

在捕获一个异常后抛出另一个异常,并且希望能把原始异常的信息保存下来:

  • ErrorExceptionRuntimeException 提供了带参数的构造器。
  • 其他类型的异常,通过 initCause() 方法实现
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
class A extends Exception {

static void f() throws Exception {
A a = new A();
a.initCause(new NullPointerException());
throw a;
}

static void g() throws Exception {
A a = new A();
throw new RuntimeException(a);
}
}

try {
A.f();
} catch (Exception e) {
e.printStackTrace();
}

// 输出:
/*
A
at A.f(Main.java:26)
at Main.main(Main.java:14)
... 5 more
Caused by: java.lang.NullPointerException
at A.f(Main.java:27)
... 6 more
*/


try {
A.g();
} catch (Exception e) {
e.printStackTrace();
}

// 输出:
/*
java.lang.RuntimeException: A
at A.g(Main.java:33)
at Main.main(Main.java:14)
... 5 more
Caused by: A
at A.g(Main.java:32)
... 6 more
*/

Java 标准异常

Throwable 被用来表示任何可以作为异常被抛出的类,其对象可以分成两类:

  • Error,用来表示编译时错误和系统错误
  • Exception,是可以被抛出的基本类型

RuntimeException 类型的异常,也被称作 不受检查异常,这种异常不需要编译器强制进行检查。如果 RuntimeException 没有被捕获而直达 main(),那么在程序退出前将自动调用异常的 printStackTrace() 方法。

异常丢失

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
try {
throw new RuntimeException();
}finally {
throw new NullPointerException();
}

// 输出:
/*
Exception in thread "main" java.lang.NullPointerException
at Main.main(Main.java:16)
... 5 more
*/


// 把所有抛出的异常打包到 try-catch 子句中
try {
throw new RuntimeException();
} finally {
try {
throw new NullPointerException();
} catch (Exception e) {
e.printStackTrace();
}
}

// 输出:
/*
java.lang.NullPointerException
at Main.main(Main.java:17)
... 5 more
Exception in thread "main" java.lang.RuntimeException
at Main.main(Main.java:14)
... 5 more
*/

finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {

static void f() {
try {
System.out.println(1);
return;
} finally {
System.out.println(2);
}
}
}

A.f();

// 输出:
/*
1
2
*/

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
class A {

static void f(int k) {

while (true) {
try {
System.out.println(k++);
if (k == 2) {
break;
}
} finally {
System.out.println(2);
}
}
}
}

A.f(0);

// 输出:
/*
0
2
1
2
*/

异常限制

当覆盖方法时,只能抛出在基类方法的异常说明里列出的那些异常。

异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此,不能基于异常说明来重载方法。此外,一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AException extends Exception {
}

class BException extends Exception {
}

class CException extends Exception {
}

interface A {
void f() throws AException;
}

interface B {
void f() throws BException;
}

class K implements A, B {

// 不能声明异常 AException,BException,CException
@Override
public void f() {
}
}

构造器

涉及到构造器时,尽量不要在 finally 里面进行清理。因为很可能构造器在执行过程中就抛出异常,导致部分对象没有被成功创建。 尽量使用 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
class InputFile {

private BufferedReader in;

public InputFile(String fname) throws Exception {

try {
// 抛出 FileNotFoundException 异常时,代表没有成功,也就不需要关闭文件
// 抛出其他异常,需要关闭文件
in = new BufferedReader(new FileReader(fname));
} catch (FileNotFoundException e) {
throw e;
} catch (Exception e) {
try {
// 关闭文件也可能会抛出异常
in.close();
} catch (IOException ex) {
System.out.println("in.close() failed");
}
throw e;
}
}
}

异常使用指南

应该在下列情况下使用异常:

  • 在恰当的级别处理问题(在知道该如何处理的情况下,才去捕获异常)
  • 解决问题并且重新调用产生异常的方法
  • 进行少许修补,然后绕过异常发生的地方继续执行
  • 用别的数据进行计算,以代替方法预计会返回的值
  • 把当前运行环境下能够做的事情尽量做完,然后把相同的(或者不同的)异常抛到更高层
  • 终止程序
  • 进行简化
  • 让类库和程序更安全