Skip to content

Commit b46c314

Browse files
author
waylau
committed
completed exceptions
1 parent deb1abf commit b46c314

File tree

6 files changed

+331
-1
lines changed

6 files changed

+331
-1
lines changed

SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
* [异常捕获与处理](docs/exceptions-catch-and-handle.md)
3434
* [通过方法声明异常抛出](docs/exceptions-specify-exceptions-thrown.md)
3535
* [如何抛出异常](docs/exceptions-throw.md)
36-
* [使用 try-with-resources 声明](docs/exceptions-try-with-resources.md)
36+
* [异常链](docs/exceptions-chained-exceptions.md)
37+
* [创建异常类](docs/exceptions-create-exception-class.md)
3738
* [未检查异常](docs/exceptions-unchecked-exception.md)
3839
* [使用异常带来的优势](docs/exceptions-advantages.md)
3940
* [附录](docs/appendix.md)

docs/exceptions-advantages.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,212 @@
11
# 使用异常带来的优势
22

3+
现在你知道什么是异常,以及如何使用它们,现在是时候了解在程序中使用异常的优点。
4+
5+
## 优点1:将错误处理代码与“常规”代码分离
6+
7+
异常提供了一种方法来分离当一个程序的主逻辑发生异常情况时应该做什么的细节。 在传统的编程中,错误检测、报告和处理常常导致混淆意大利面条代码(spaghetti code)。 例如,考虑这里的伪代码方法将整个文件读入内存。
8+
9+
```java
10+
readFile {
11+
open the file;
12+
determine its size;
13+
allocate that much memory;
14+
read the file into memory;
15+
close the file;
16+
}
17+
```
18+
19+
乍一看,这个功能看起来很简单,但它忽略了以下所有潜在错误。
20+
21+
* 如果无法打开文件会发生什么?
22+
* 如果无法确定文件的长度,会发生什么?
23+
* 如果不能分配足够的内存,会发生什么?
24+
* 如果读取失败会发生什么?
25+
* 如果文件无法关闭会怎么样?
26+
27+
28+
为了处理这种情况,readFile函数必须有更多的代码来执行错误检测\报告和处理。 这里是一个示例,来展示该函数可能会是什么样子。
29+
30+
31+
```java
32+
errorCodeType readFile {
33+
initialize errorCode = 0;
34+
35+
open the file;
36+
if (theFileIsOpen) {
37+
determine the length of the file;
38+
if (gotTheFileLength) {
39+
allocate that much memory;
40+
if (gotEnoughMemory) {
41+
read the file into memory;
42+
if (readFailed) {
43+
errorCode = -1;
44+
}
45+
} else {
46+
errorCode = -2;
47+
}
48+
} else {
49+
errorCode = -3;
50+
}
51+
close the file;
52+
if (theFileDidntClose && errorCode == 0) {
53+
errorCode = -4;
54+
} else {
55+
errorCode = errorCode and -4;
56+
}
57+
} else {
58+
errorCode = -5;
59+
}
60+
return errorCode;
61+
}
62+
```
63+
64+
这里面会有很多错误检测、报告的细节,使得原来的七行代码被淹没在这杂乱的代码中。 更糟的是,代码的逻辑流也已经丢失,因此很难判断代码是否正确:如果函数无法分配足够的内存,文件是否真的被关闭? 在编写方法三个月后修改方法时,更难以确保代码能够继续正确的操作。 因此,许多程序员通过简单地忽略它来解决这个问题。这样当他们的程序崩溃时,就生成了报告错误。
65+
66+
异常使您能够编写代码的主要流程,并处理其他地方的特殊情况。 如果readFile函数使用异常而不是传统的错误管理技术,它将看起来更像下面。
67+
68+
```java
69+
readFile {
70+
try {
71+
open the file;
72+
determine its size;
73+
allocate that much memory;
74+
read the file into memory;
75+
close the file;
76+
} catch (fileOpenFailed) {
77+
doSomething;
78+
} catch (sizeDeterminationFailed) {
79+
doSomething;
80+
} catch (memoryAllocationFailed) {
81+
doSomething;
82+
} catch (readFailed) {
83+
doSomething;
84+
} catch (fileCloseFailed) {
85+
doSomething;
86+
}
87+
}
88+
```
89+
90+
91+
请注意,异常不会减少你在法执行检测、报告和处理错误方面的工作,但它们可以帮助您更有效地组织工作。
92+
93+
## 优点2:将错误沿调用推栈向上传递
94+
95+
异常的第二个优点是能够在方法的调用堆栈上将错误向上传递。 假设 readFile 方法是由主程序进行的一系列嵌套方法调用中的第四个方法:method1调用method2,它调用了method3,最后调用readFile。
96+
97+
```java
98+
method1 {
99+
call method2;
100+
}
101+
102+
method2 {
103+
call method3;
104+
}
105+
106+
method3 {
107+
call readFile;
108+
}
109+
```
110+
111+
还假设method1是对readFile中可能发生的错误感兴趣的唯一方法。 传统的错误通知技术强制method2和method3将readFile返回的错误代码传递到调用堆栈,直到错误代码最终到达method1 - 对它们感兴趣的唯一方法。
112+
113+
```java
114+
method1 {
115+
errorCodeType error;
116+
error = call method2;
117+
if (error)
118+
doErrorProcessing;
119+
else
120+
proceed;
121+
}
122+
123+
errorCodeType method2 {
124+
errorCodeType error;
125+
error = call method3;
126+
if (error)
127+
return error;
128+
else
129+
proceed;
130+
}
131+
132+
errorCodeType method3 {
133+
errorCodeType error;
134+
error = call readFile;
135+
if (error)
136+
return error;
137+
else
138+
proceed;
139+
}
140+
```
141+
142+
回想一下,Java运行时环境通过调用堆栈向后搜索以找到任何对处理特定异常感兴趣的方法。 一个方法可以阻止在其中抛出的任何异常,从而允许一个方法在调用栈上更远的地方来捕获它。 因此,只有关心错误的方法才需要担心检测错误。
143+
144+
```java
145+
method1 {
146+
try {
147+
call method2;
148+
} catch (exception e) {
149+
doErrorProcessing;
150+
}
151+
}
152+
153+
method2 throws exception {
154+
call method3;
155+
}
156+
157+
method3 throws exception {
158+
call readFile;
159+
}
160+
```
161+
162+
然而,如伪代码所示,抛弃异常需要中间人方法的一些努力。 任何可以在方法中抛出的已检查异常都必须在其throws子句中指定。
163+
164+
165+
166+
## 优点3:对错误类型进行分组和区分
167+
168+
169+
因为在程序中抛出的所有异常都是对象,异常的分组或分类是类层次结构的自然结果。 Java平台中一组相关异常类的示例是java.io - IOException中定义的那些异常类及其后代。 IOException是最常见的,表示执行I/O时可能发生的任何类型的错误。 它的后代表示更具体的错误。 例如,FileNotFoundException意味着文件无法在磁盘上找到。
170+
171+
一个方法可以编写可以处理非常特定异常的特定处理程序。 FileNotFoundException类没有后代,因此下面的处理程序只能处理一种类型的异常。
172+
173+
```java
174+
catch (FileNotFoundException e) {
175+
...
176+
}
177+
```
178+
179+
方法可以通过在catch语句中指定任何异常的超类来基于其组或常规类型捕获异常。 例如,为了捕获所有I/O异常,无论其具体类型如何,异常处理程序都会指定一个IOException参数。
180+
181+
```java
182+
catch (IOException e) {
183+
...
184+
}
185+
```
186+
187+
这个处理程序将能够捕获所有I/O异常,包括FileNotFoundException、EOFException等等。 您可以通过查询传递给异常处理程序的参数来查找有关发生的详细信息。 例如,使用以下命令打印堆栈跟踪。
188+
189+
```java
190+
catch (IOException e) {
191+
// Output goes to System.err.
192+
e.printStackTrace();
193+
// Send trace to stdout.
194+
e.printStackTrace(System.out);
195+
}
196+
```
197+
198+
199+
下面例子可以处理所有的异常:
200+
201+
```java
202+
// A (too) general exception handler
203+
catch (Exception e) {
204+
...
205+
}
206+
```
207+
208+
Exception 类接近Throwable类层次结构的顶部。因此,这个处理程序将会捕获除处理程序想要捕获的那些异常之外的许多其他异常。在程序中如果是以这种方式来处理异常,那么你程序一般的做法就是,例如,是打印出一个错误消息给用户,然后退出。
209+
210+
在大多数情况下,异常处理程序应该尽可能的具体。原因是处理程序必须做的第一件事是在选择最佳恢复策略之前,首先要确定发生的是什么类型的异常。实际上,如果不捕获特定的错误,处理程序必须适应任何可能性。太过通用的异常处理程序可能会捕获和处理程序员不期望的并且处理程序不想要的异常,从而使代码更容易出错。
211+
212+
如上所述,您可以以常规方式创建异常分组来处理异常,也可以使用特定的异常类型来区分异常从而可以以确切的方式来处理异常。
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# 异常链
2+
3+
应用程序通常会通过抛出另一个异常来响应异常。 实际上,第一个异常引起第二个异常。 它可以是非常有助于用户知道什么时候一个异常导致另一个异常。 “异常链(Chained Exceptions)”帮助程序员做到这一点。
4+
5+
以下是Throwable中支持异常链的方法和构造函数。
6+
7+
```java
8+
Throwable getCause()
9+
Throwable initCause(Throwable)
10+
Throwable(String, Throwable)
11+
Throwable(Throwable)
12+
```
13+
14+
15+
16+
initCause和Throwable构造函数的Throwable参数是导致当前异常的异常。 getCause返回导致当前异常的异常,initCause设置当前异常的原因。
17+
18+
以下示例显示如何使用异常链。
19+
20+
```java
21+
try {
22+
23+
} catch (IOException e) {
24+
throw new SampleException("Other IOException", e);
25+
}
26+
```
27+
28+
29+
在此示例中,当捕获到IOException时,将创建一个新的SampleException异常,并附加原始的异常原因,并将异常链抛出到下一个更高级别的异常处理程序。
30+
31+
## 访问堆栈跟踪信息
32+
33+
现在让我们假设更高级别的异常处理程序想要以自己的格式转储堆栈跟踪。
34+
35+
定义:堆栈跟踪(stack trace)提供有关当前线程的执行历史的信息,并列出在异常发生时调用的类和方法的名称。 堆栈跟踪是一个有用的调试工具,通常在抛出异常时会利用它。
36+
37+
以下代码显示了如何在异常对象上调用getStackTrace方法。
38+
39+
```java
40+
catch (Exception cause) {
41+
StackTraceElement elements[] = cause.getStackTrace();
42+
for (int i = 0, n = elements.length; i < n; i++) {
43+
System.err.println(elements[i].getFileName()
44+
+ ":" + elements[i].getLineNumber()
45+
+ ">> "
46+
+ elements[i].getMethodName() + "()");
47+
}
48+
}
49+
```
50+
51+
## 日志 API
52+
53+
54+
如果要记录catch块中所发生异常,最好不要手动解析堆栈跟踪并将输出发送到 System.err(),而是使用[java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html)包中的日志记录工具将输出发送到文件。
55+
56+
57+
```java
58+
try {
59+
Handler handler = new FileHandler("OutFile.log");
60+
Logger.getLogger("").addHandler(handler);
61+
62+
} catch (IOException e) {
63+
Logger logger = Logger.getLogger("package.name");
64+
StackTraceElement elements[] = e.getStackTrace();
65+
for (int i = 0, n = elements.length; i < n; i++) {
66+
logger.log(Level.WARNING, elements[i].getMethodName());
67+
}
68+
}
69+
```
70+
71+
72+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# 创建异常类
2+
3+
当面对选择抛出异常的类型时,您可以使用由别人编写的异常 - Java平台提供了许多可以使用的异常类 - 或者您可以编写自己的异常类。 如果您对任何以下问题回答“是”,您应该编写自己的异常类;否则,你可以使用别人的。
4+
5+
* 你需要一个Java平台中没有表示的异常类型吗?
6+
* 如果用户能够区分你的异常与由其他供应商编写的类抛出的异常吗?
7+
* 你的代码是否抛出不止一个相关的异常?
8+
* 如果您使用他人的例外,用户是否可以访问这些异常? 一个类似的问题是你的包是独立只提供自己使用吗?
9+
10+
11+
## 一个例子
12+
13+
假设你正在写一个链表类。该类支持以下方法:
14+
15+
* objectAt(int n) - 返回列表中第n个位置的对象。如果参数小于0或大于当前列表中的对象数,则抛出异常。
16+
* firstObject() - 返回列表中的第一个对象。如果列表不包含对象,则抛出异常。
17+
* indexOf(Object o) - 搜索指定对象的列表,并返回其在列表中的位置。如果传入方法的对象不在列表中,则抛出异常。
18+
19+
链表类可以抛出多个异常,使用一个异常处理程序捕获链表所抛出的所有异常是很方便的。此外,如果您计划在包中分发链表,所有相关代码都应打包在一起。因此,链表应该提供自己的一组异常类。
20+
21+
下图说明了链表抛出的异常的一个可能的类层次结构。
22+
23+
![](../images/exception/exceptions-hierarchy.gif)
24+
25+
26+
## 选择超类
27+
28+
任何 Exception 子类都可以用作 LinkedListException 的父类。 然而,但这些子类有些专用的,有些又与 LinkedListException 完全无关。 因此,LinkedListException的父类应该是Exception。
29+
30+
你编写的大多数applet和应用程序都会抛出 Exception 对象。 Error 通常用于系统中严重的硬错误,例如阻止JVM运行的错误。
31+
32+
注意:对于可读代码,最好将字符串Exception附加到从异常类继承(直接或间接)的所有类的名称。
33+
34+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,15 @@
11
# 未检查异常
22

3+
因为Java编程语言不需要捕获方法或声明未检查异常(包括 RuntimeException、Error及其子类),程序员可能会试图编写只抛出未检查异常的代码,或使所有异常子类继承自RuntimeException。这两个快捷方式都允许程序员编写代码,而不必担心编译器错误,也不用担心声明或捕获任何异常。虽然这对于程序员似乎很方便,但它避开了捕获或者声明异常的需求,并且可能会导致其他人在使用您的类而产生问题。
4+
5+
为什么设计人员决定强制一个方法来指定所有可以抛出的未捕获的已检查异常?任何可以由方法抛出的 Exception 都是方法的公共编程接口的一部分。调用方法的人必须知道一个方法可以抛出的异常,以便他们可以决定如何处理它们。这些异常是该方法的编程接口的一部分,作为它的参数和 return 值。
6+
7+
下一个问题可能是:“既然一个方法的API已经做好了很好的记录,包括它可以抛出的异常,为什么不指定运行时异常?”运行时异常展示的是编程问题的结果,因此,API用户可能会用不合理方式来处理它们。这样就有可能产生问题,包括算术异常,例如除以零;指针异常,例如试图通过空引用访问对象;索引异常,例如尝试通过太大或太小的索引访问数组元素。
8+
9+
运行时异常可能发生在程序中的任何地方,在典型的程序中它们可以非常多。必须在每个方法声明中添加运行时异常则会降低程序的清晰度。因此,编译器不需要捕获或声明运行时异常(尽管可以是可以做到)。
10+
11+
一种情况是,通常的做法是当用户调用一个方法不正确时,抛出一个RuntimeException。例如,一个方法可以检查其中一个参数是否不正确为null。如果参数为null,那么该方法可能会抛出NullPointerException异常,这是一个未检查异常。
12+
13+
一般来说,不要抛出一个RuntimeException或创建一个RuntimeException的子类,这样你就不会被声明哪些方法可以抛出的异常所困扰。
14+
15+
一个底线原则是:如果客户端可以合理地从期望异常中恢复,那么使其成为一个已检查异常。如果客户端无法从异常中恢复,请将其设置为未检查异常。
10 KB
Loading

0 commit comments

Comments
 (0)