Java 的基本理念是“结构不佳的代码不能运行”。
改进的错误恢复机制是提高代码健壮性的最强有力的方式。
发现错误的理想时机是在编译阶段,然而,编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。
要想创建健壮的系统,它的每一个构件都必须是健壮的。
异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器强制执行。本章将向读者介绍如何编写正确的异常处理程序,并将展示当方法出问题的时候,如何产生自定义的异常。
C 以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。
对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。解决的办法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。
异常往往能降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。
异常机制使代码的阅读、编写和调试工作更加井井有条。
异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的问题。
当抛出异常后,有几件事会随之发生。首先,同 Java 中其他对象的创建一样,将使用 new 在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。
异常最重要的方面之一就是如果发生问题,它们将不允许程序沿着其正常的路径继续走下去。
与使用 Java 中的其他对象一样,我们总是用 new 在堆上创建异常对象,这也伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器:
throw new NullPointerException("t = null");
在使用 new 创建了异常对象之后,此对象的引用将传给 throw。尽管异常对象的类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法“返回”的。
另外还能用抛出异常的方式从当前的作用域退出。
此外,能够抛出任意类型的 Throwable 对象,它是异常类型的根类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。
要明白异常是如何被捕获的,必须首先理解监控区域(guarded region)的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
如果在方法内部抛出了异常(或者在方法内部调用的其他方法抛出了异常),这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为在这个块里“尝试”各种(可能产生异常的)方法调用,所以称为 try 块。它是跟在 try 关键字之后的普通程序块:
try {
// Code that might generate exceptions
}
当然,抛出的异常必须在某处得到处理。这个“地点”就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在 try 块之后,以关键字 catch 表示:
try {
// Code that might generate exceptions
} catch (Type1 id1) {
// Handle exceptions of Type1
} catch (Type2 id2) {
// Handle exceptions of Type2
} catch (Type3 id3) {
// Handle exceptions of Type3
}
// etc.
异常处理程序必须紧跟在 try 块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束。注意,只有匹配的 catch 子句才能得到执行。
注意在 try 块的内部,许多不同的方法调用可能会产生类型相同的异常,而你只需要提供一个针对此类型的异常处理程序。
异常处理理论上有两种基本模型。Java 支持终止模型(它是 Java 和 C++所支持的模型)。在这种模型中,将假设错误非常严重,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
另一种称为恢复模型。意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。
虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。
要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生无参构造器,所以这几乎不用写多少代码:
class SimpleException extends Exception {}
public class InheritingExceptions {
public void f() throws SimpleException {
System.out.println("Throw SimpleException from f()");
throw new SimpleException();
}
public static void main(String[] args) {
InheritingExceptions sed = new InheritingExceptions();
try {
sed.f();
} catch (SimpleException e) {
System.out.println("Caught it!");
}
}
}
输出:
Throw SimpleException from f()
Caught it!
—PS:不能无中生有
编译器创建了无参构造器,它将自动调用基类的无参构造器。
可以为异常类创建一个接受字符串参数的构造器:
class MyException extends Exception {
MyException() {
}
MyException(String msg) {
super(msg);
}
}
public class FullConstructors {
public static void f() throws MyException {
System.out.println("Throwing MyException from f()");
throw new MyException();
}
public static void g() throws MyException {
System.out.println("Throwing MyException from g()");
throw new MyException("Originated in g()");
}
public static void main(String[] args) {
try {
f();
} catch (MyException e) {
e.printStackTrace(System.out);
}
try {
g();
} catch (MyException e) {
e.printStackTrace(System.out);
}
}
}
输出:
Throwing MyException from f()
com.yichang.MyException
at com.yichang.FullConstructors.f(FullConstructors.java:20)
at com.yichang.FullConstructors.main(FullConstructors.java:30)
Throwing MyException from g()
com.yichang.MyException: Originated in g()
at com.yichang.FullConstructors.g(FullConstructors.java:25)
at com.yichang.FullConstructors.main(FullConstructors.java:35)
e.printStackTrace();
将打印“从方法调用处直到异常抛出处”的方法调用序列。
—PS:简单讲,就是能帮你定位到抛异常的代码处
还可以更进一步自定义异常,比如加入额外的构造器和成员:
class MyException2 extends Exception {
private int x;
MyException2() {
}
MyException2(String msg) {
super(msg);
}
MyException2(String msg, int x) {
super(msg);
this.x = x;
}
public int val() {
return x;
}
@Override
public String getMessage() {
return "Detail Message: " + x + " " + super.getMessage();
}
}
Java 提供了相应的语法(并强制使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。
这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。
异常说明使用了附加的关键字 throws,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:
void f() throws TooBig, TooSmall, DivZero { // ...
代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java 在编译时就可以保证一定水平的异常正确性。
这种在编译时被强制检查的异常称为被检查的异常。
通过捕获异常类型的基类 Exception,就可以做到这一点:
catch (Exception e) {
System.out.println("Caught an exception");
}
这将捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常捕获了。
包含异常信息的 Exception 方法:
String getMessage()
String getLocalizedMessage()
String toString()
void printStackTrace()
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch (Exception e) {
System.out.println("Caught Exception");
System.out.println("getMessage():" + e.getMessage());
System.out.println("getLocalizedMessage():" + e.getLocalizedMessage());
System.out.println("toString():" + e);
System.out.println("printStackTrace():");
e.printStackTrace(System.out);
}
}
}
输出:
Caught Exception
getMessage():My Exception
getLocalizedMessage():My Exception
toString():java.lang.Exception: My Exception
printStackTrace():
java.lang.Exception: My Exception
at com.yichang.ExceptionMethods.main(ExceptionMethods.java:11)
可以发现每个方法都比前一个提供了更多的信息一一实际上它们每一个都是前一个的超集。
如果有一组具有相同基类的异常,你想使用同一方式进行捕获,可以直接 catch 它们的基类型。但是,如果这些异常没有共同的基类型,在 Java 7 之前,你必须为每一个类型编写一个 catch:
class EBase1 extends Exception {
}
class Except1 extends EBase1 {
}
class EBase2 extends Exception {
}
class Except2 extends EBase2 {
}
class EBase3 extends Exception {
}
class Except3 extends EBase3 {
}
class EBase4 extends Exception {
}
class Except4 extends EBase4 {
}
public class SameHandler {
void x() throws Except1, Except2, Except3, Except4 {
}
void process() {
}
void process1() {
}
void process2() {
}
// 单个捕获
void f1() {
try {
x();
} catch (Except1 e) {
process();
} catch (Except2 e) {
process();
} catch (Except3 e) {
process();
} catch (Except4 e) {
process();
}
}
// 通过 Java 7 的多重捕获机制,你可以使用“或”将不同类型的异常组合起来,
// 只需要一行 catch 语句:
void f2() {
try {
x();
} catch (Except1 | Except2 | Except3 | Except4 e) {
process();
}
}
// 组合捕获
void f3() {
try {
x();
} catch (Except1 | Except2 e) {
process1();
} catch (Except3 | Except4 e) {
process2();
}
}
}
printStackTrace() 方法所提供的信息可以通过 getStackTrace() 方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一桢。元素 0 是栈顶元素,并且是调用 序列中的最后一个方法调用(这个 Throwable 被创建和抛出之处)。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。
—PS:输出的轨迹是倒着来的
public class WhoCalled {
static void f() {
try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement ste : e.getStackTrace()) {
System.out.println(ste.getMethodName());
}
}
}
static void g() {
f();
}
static void h() {
g();
}
public static void main(String[] args) {
f();
System.out.println("*******");
g();
System.out.println("*******");
h();
}
}
输出:
f
main
*******
f
g
main
*******
f
g
h
main
这里,我们只打印了方法名,但实际上还可以打印整个 StackTraceElement,它包含其他附加的信息。
有时希望把刚捕获的异常重新抛出,尤其是在使用 Exception 捕获所有异常的时候。
catch(Exception e) {
System.out.println("An exception was thrown");
throw e;
}
重抛异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。此外,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。
如果只是把当前异常对象重新抛出,那么 printStackTrace() 方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用 filInStackTrace() 方法,这将返回一个 Throwable 对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的,就像这样:
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside g(), e.printStackTrace()");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside h(), e.printStackTrace()");
e.printStackTrace(System.out);
throw (Exception) e.fillInStackTrace();
}
}
public static void main(String[] args) {
try {
g();
} catch (Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
System.out.println("-----------------");
try {
h();
} catch (Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}
输出:
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
at com.yichang.Rethrowing.f(Rethrowing.java:11)
at com.yichang.Rethrowing.g(Rethrowing.java:16)
at com.yichang.Rethrowing.main(Rethrowing.java:36)
main: printStackTrace()
java.lang.Exception: thrown from f()
at com.yichang.Rethrowing.f(Rethrowing.java:11)
at com.yichang.Rethrowing.g(Rethrowing.java:16)
at com.yichang.Rethrowing.main(Rethrowing.java:36)
-----------------
originating the exception in f()
Inside h(), e.printStackTrace()
java.lang.Exception: thrown from f()
at com.yichang.Rethrowing.f(Rethrowing.java:11)
at com.yichang.Rethrowing.h(Rethrowing.java:26)
at com.yichang.Rethrowing.main(Rethrowing.java:43)
main: printStackTrace()
java.lang.Exception: thrown from f()
at com.yichang.Rethrowing.h(Rethrowing.java:30)
at com.yichang.Rethrowing.main(Rethrowing.java:43)
有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用 filInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息。
在 Java 7 之前,如果遇到异常,则只能重新抛出该类型的异常。这导致在 Java 7 中修复的代码不精确。所以在 Java 7 之前,这无法编译:
class BaseException extends Exception {
}
class DerivedException extends BaseException {
}
public class PreciseRethrow {
void catcher() throws DerivedException {
try {
throw new DerivedException();
} catch (BaseException e) {
throw e;
}
}
}
因为 catch 捕获了一个 BaseException,编译器强迫你声明 catcher() 抛出 BaseException,即使它实际上抛出了更具体的 DerivedException。从 Java 7 开始,这段代码就可以编译,这是一个很小但很有用的修复。
常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。
Throwable 这个 Java 类被用来表示任何可以作为异常被抛出的类。Throwable 对象可分为两种类型(指从 Throwable 继承而得到的类型):
—PS:Error 一般不用管,也管不了
属于运行时异常的类型有很多,它们会自动被 java 虚拟机抛出,所以不必在异常说明中把它们列出来。这些异常都是从 RuntimeException 类继承而来,所以既体现了继承的优点,使用起来也很方便。
这构成了一组具有相同特征和行为的异常类型。并且,也不再需要在异常说明中声明方法将抛出 RuntimeException 类型的异常(或者任何从 RuntimeException 继承的异常),它们也被称为“不受检查异常”。这种异常属于错误,将被自动捕获,就不用你亲自动手了。
RuntimeException 代表的是编程错误:
无法预料的错误。比如从你控制范围之外传递进来的 null 引用。
作为程序员,应该在代码中进行检查的错误。(比如对于 ArrayIndexOutOfBoundsException,就得注意一下数组的大小了。)
有一些代码片段,可能会希望无论 try 块中的异常是否抛出,它们都能得到执行。为了达到这个效果,可以在异常处理程序后面加上 finally 子句。
try{
// The guarded region: Dangerous activities
}catch(A a1){
// Handler for situation A
}catch(B b1){
// Handler for situation B
}finally{
// Activities that happen every time
}
class ThreeException extends Exception {
}
public class FinallyWorks {
static int count = 0;
public static void main(String[] args) {
while (true) {
try {
if (count++ == 0) {
throw new ThreeException();
}
System.out.println("No exception");
} catch (ThreeException e) {
System.out.println("ThreeException");
} finally {
System.out.println("In finally clause");
if (count == 2) {
break;
}
}
}
}
}
输出:
ThreeException
In finally clause
No exception
In finally clause
对于没有垃圾回收和析构函数自动调用机制的语言来说,finally 用来释放内存。
对于 Java 当要把除内存之外的资源恢复到它们的初始状态时,就要用到 finally 子句。
这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。
—PS:主要用于关闭流
因为 finally 子句总是会执行,所以可以从一个方法内的多个点返回,仍然能保证重要的清理工作会执行:
public class MultipleReturns {
public static void f(int i) {
System.out.println("Initialization that requires cleanup");
try {
System.out.println("Point 1");
if (i == 1) return;
System.out.println("Point 2");
if (i == 2) return;
System.out.println("Point 3");
if (i == 3) return;
System.out.println("End");
return;
} finally {
System.out.println("Performing cleanup");
}
}
public static void main(String[] args) {
for (int i = 1; i <= 4; i++) {
f(i);
}
}
}
输出:
Initialization that requires cleanup
Point 1
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
End
Performing cleanup
遗憾的是,Java 的异常实现也有瑕疵。异常作为程序出错的标志,决不应该被忽略,但它还是有可能被轻易地忽略。用某些特殊的方式使用 finally 子句,就会发生这种情况。
—PS:在 finally 中抛出个新异常或直接 return ,都会丢失上一个异常
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。
异常限制对构造器不起作用。
—PS:覆盖方法时,子类方法只能抛比父类方法异常更少,可以不抛异常,也可以抛子异常。可少不可多。
—PS:构造器异常列表必须包含父构造器异常列表。可多不可少
对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的 try 子句。
不使用该方法的例子:
import java.io.*;
public class MessyExceptions {
public static void main(String[] args) {
InputStream in = null;
try {
in = new FileInputStream(new File("MessyExceptions.java"));
int contents = in.read();
// Process contents
} catch (IOException e) {
// Handle the error
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// Handle the close() error
}
}
}
}
}
使用该方法的例子:
import java.io.*;
public class TryWithResources {
public static void main(String[] args) {
try (
InputStream in = new FileInputStream
(new File("TryWithResources.java"))
) {
int contents = in.read();
// Process contents
} catch (IOException e) {
// Handle the error
}
}
}
try 跟一个带括号的定义( 这里是我们创建的 FileInputStream 对象)。括号内的部分称为资源规范头(resource specification header)。现在可用于整个 try 块的其余部分。更重要的是,无论你如何退出 try 块(正常或异常),都会执行前一个 finally 子句的等价物,但不会编写那些杂乱而棘手的代码。
—PS:try 括号里面创建一个资源,默认就有一个 finally 去关闭
资源规范头中可以包含多个定义,并且通过分号进行分割(最后一个分号是可选的)。规范头中定义的每个对象都会在 try 语句块运行结束之后调用 close() 方法
它是如何工作的?在 try-with-resources 定义子句中创建的对象(在括号内)必须实现 java.lang.AutoCloseable 接口,这个接口有一个方法:close()。当在 Java 7 中引入 AutoCloseable 时,许多接口和类被修改以实现它;
class Reporter implements AutoCloseable {
String name = getClass().getSimpleName();
public Reporter() {
System.out.println("Creating " + name);
}
@Override
public void close() {
System.out.println("Closing " + name);
}
}
class First extends Reporter {
}
class Second extends Reporter {
}
public class AutoCloseableDetails {
public static void main(String[] args) {
try (First first = new First();
Second second = new Second();) {
}
}
}
输出:
Creating First
Creating Second
Closing Second
Closing First
以上是解析原理的例子。
退出 try 块会调用两个对象的 close() 方法,并以与创建顺序相反的顺序关闭它们。
假设我们在资源规范头中定义了一个不是 AutoCloseable 的对象会出现编译时错误。
抛出异常的时候,异常处理系统会按照代码的书写顺序找出==“最近”==的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。
查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序,就像这样:
class Annoyance extends Exception {
}
class Sneeze extends Annoyance {
}
public class Human {
public static void main(String[] args) {
// 精准匹配
try {
throw new Sneeze();
} catch (Sneeze e) {
System.out.println("Caught Sneeze");
} catch (Annoyance a) {
System.out.println("Caught Annoyance");
}
// 匹配基类异常
try {
throw new Sneeze();
} catch (Annoyance a) {
System.out.println("Caught Annoyance");
}
}
}
输出:
Caught Sneeze
Caught Annoyance
如果把捕获基类的 catch 子句放在最前面,会报告错误
如果发现有些“被检查的异常”挡住了路,尤其是发现你不得不去对付那些不知道该如何处理的异常,还是有些办法的:
可以直接把“被检查的异常”包装进 RuntimeException 里面
应该在下列情况下使用异常:
尽可能使用 try-with-resource。
在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
解决问题并且重新调用产生异常的方法。
进行少许修补,然后绕过异常发生的地方继续执行。
用别的数据进行计算,以代替方法预计会返回的值。
把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
终止程序。
进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)
异常是 Java 程序设计不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。
异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一处处理你编写的这段代码中产生的错误。
Java 坚定地强调将所有的错误都以异常形式报告的这一事实,正是它远远超过语如 C++ 这类语言的长处之一。
自我学习总结:
throw new Exception();
try {
// Code that might generate exceptions
} catch (Type1 id1) {
// Handle exceptions of Type1
} catch (Type2 id2) {
// Handle exceptions of Type2
} catch (Type3 id3) {
// Handle exceptions of Type3
}
// etc.
void f() throws TooBig, TooSmall, DivZero { // ...
String getMessage()
String getLocalizedMessage()
String toString()
void printStackTrace()
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)
每个方法都比前一个方法提供的信息多
catch (Except1 | Except2 | Except3 | Except4 e)
try{
// The guarded region: Dangerous activities
}catch(A a1){
// Handler for situation A
}catch(B b1){
// Handler for situation B
}finally{
// Activities that happen every time
}
try(资源规范头){}catch(Exception e){}
特别是流相关代码时,推荐使用