6.2.异常引发

\(6.2\)异常引发

1.\(throw\)语句

  考虑下面这个实现\(ArraySet\)的程序:

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
import java.util.Iterator;

public class ArraySet<T> implements Iterable<T> {
private T[] items;
private int size; // the next item to be added will be at position size

public ArraySet() {
items = (T[]) new Object[100];
size = 0;
}

/* Returns true if this map contains a mapping for the specified key.
*/
public boolean contains(T x) {
for (int i = 0; i < size; i += 1) {
if (items[i].equals(x)) {
return true;
}
}
return false;
}

/* Associates the specified value with the specified key in this map. */
public void add(T x) {
if (contains(x)) {
return;
}
items[size] = x;
size += 1;
}

/* Returns the number of key-value mappings in this map. */
public int size() {
return size;
}
}

  这个程序有一个小问题:当我们向\(ArraySet\)中加入\(null\)时,会产生NullPointerException的错误。这是因为:在contains方法中,由于加入了\(null\),调用\(items[i]\)时会产生这种错误。

  我们可以选择抛出自己的异常(\(exceptions\))。在\(python\)中我们使用\(raise\)语句,而在\(Java\)中,我们可以用如下语句声明错误:

1
throw new ExceptionObject(parameter1, ...)

  于是,我们可以如下编写我们的异常抛弃(\(throwing\;exception\))语句:

1
2
3
4
5
6
7
8
9
10
11
12
/* Associates the specified value with the specified key in this map.
Throws an IllegalArgumentException if the key is null. */
public void add(T x) {
if (x == null) {
throw new IllegalArgumentException("can't add null");
}
if (contains(x)) {
return;
}
items[size] = x;
size += 1;
}

  虽然不管有没有\(throw\)语句,程序都会在该处中断。但是,通过\(throw\)语句,我们对程序有了更高的掌控力:我们可以自己决定程序应该在何处中断。同时,我们也可以将\(Java\)中的报错更改为更具体的语句。

2.\(catch\)语句

  利用先前的\(throw\),我们可以将\(Java\)中的隐性错误(\(implicit\;exceptions\))显式化,例如:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
System.out.println("ayyy lmao");
throw new RuntimeException("For no reason.");
}

$ java Alien
ayyy lmao
Exception in thread "main" java.lang.RuntimeException: For no reason.
at Alien.main(Alien.java:4)

  这里,我们发现了构造方法new,这看起来和类的实例化非常相似——事实上就是如此。\(RuntimeException\)也是一个普通的\(Java\)实例。

  因此,我们可以\(catch\)每个异常实例,如下面的例子:

1
2
3
4
5
6
7
8
9
Dog d = new Dog("Lucy", "Retriever", 80);
d.becomeAngry();

try {
d.receivePat();
} catch (Exception e) {
System.out.println("Tried to pat: " + e);
}
System.out.println(d);

  其对应输出可能为:

1
2
3
$ java ExceptionDemo
Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy is a displeased Retriever weighing 80.0 standard lb units.

  可以看到,我们在\(catch\)RuntimeException后,依然执行到了最后一行。

  我们还可以用\(catch\)语句来执行修正措施:

1
2
3
4
5
6
7
8
9
10
11
12
Dog d = new Dog("Lucy", "Retriever", 80);
d.becomeAngry();

try {
d.receivePat();
} catch (Exception e) {
System.out.println(
"Tried to pat: " + e);
d.eatTreat("banana");
}
d.receivePat();
System.out.println(d);

  在这个程序中,我们在发现异常时用\(treat\)来慰抚\(Dog\),这样,程序的异常就解决了:

1
2
3
4
5
6
7
$ java ExceptionDemo
Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy munches the banana

Lucy enjoys the pat.

Lucy is a happy Retriever weighing 80.0 standard lb units.

  In the real world, this corrective action might be extending an antenna on a robot when an exception is thrown by an operation expecting a ready antenna. Or perhaps we simply want to write the error to a log file for later analysis.

3.异常处理的哲学

  异常处理使得我们在概念上将错误处理(\(error\;handling\))与剩余部分的程序分离开来。例如,考虑下面的程序:

1
2
3
4
5
6
7
func readFile: {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}

  如果不进行异常处理,我们可能会对程序进行如下改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func readFile: {
open the file;
if (theFileIsOpen) {
determine its size;
if (gotTheFileLength) {
allocate that much memory;
} else {
return error("fileLengthError");
}
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
return error("readError");
}
...
} else {
return error("memoryError");
}
} else {
return error("fileOpenError")
}
}

  这样的代码十分冗杂。我们可以用异常处理如下书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func readFile: {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}

  异常处理的方法让代码十分整洁:首先,程序先尝试执行所需的操作;然后,程序开始捕捉所有的错误。

  Good code feels like a story; it has a certain beauty to its construction. That clarity makes it easier to both write and maintain over time.

4.未被捕捉的异常

  当一个异常被引发时,它会向下历经如下的栈:

  如果三个方法都没有捕捉到异常,程序就会中断,\(Java\)会给用户发送信息,并打印堆栈跟踪的信息:

1
2
3
4
java.lang.RuntimeException in thread “main”: 
at ArrayRingBuffer.peek:63
at GuitarString.sample:48
at GuitarHeroLite.java:110