第12章 通过异常处理错误
发现错误的理想时机是在编译阶段,但在编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。Java使用异常处理作为唯一的错误报告机制,并通过编译器强制执行。
12.1 概念
异常处理程序:将程序从错误状态中恢复,以使程序继续运行或换一种方式运行下去的代码块。
使用异常的好处是:降低错误处理代码的复杂度,将正常执行程序和异常处理程序相分离,使代码的阅读、编码和调试工作更加井井有条。
12.2 基本异常
普通问题:在当前环境下能够得到足够的信息,总能处理该错误。异常情形:阻止当前方法或作用域继续执行的问题。如果在当前环境下无法获得必要的信息来解决问题,需要抛出异常:从当前环境跳出,并把问题提交给上一级环境。
抛出异常所发生的几件事情:
- 使用new在堆上创建异常对象。
- 当前的执行路径被终止,并从当前环境中弹出对异常对象的引用。
- 异常处理机制接管程序,并寻找异常处理程序。
下例则通过创建一个代表错误信息的对象,并且将它从当前环境中抛出:
if(t == null)
throw new NullPointerException();
此时,在当前环境中则不必担心该问题了,它将在别的地方得到处理。
我们可以将异常看作是一种内建的恢复系统,并且我们可以在程序中拥有各种不同的恢复点。如果程序的某部分失败了,异常将恢复到程序中某个已知的稳定点上。
12.2.1 异常参数
与使用Java中的其他对象一样,我们同样使用new在堆上创建异常对象,并伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器:默认构造器、接收字符串作为参数的构造器:
throw new NullPointerException("t = null");
关键字throw:返回一个异常对象,并退出方法或作用域。
异常被解决的位置在一个恰当的异常处理程序中,该位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层次。
此外,被抛出的异常可以是任意类型的Throwable对象,它是异常类型的根类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。
12.3 捕获异常
监控区域:一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
12.3.1 try块
如果在方法内部抛出了异常,这个方法将在抛出异常后结束。要是不希望该方法就此结束,则可以在方法内设置一个特殊的块来捕获异常。 此块是跟随在try关键字后的普通程序块,例如:
try {
// Code that might generate exceptions
}
对于不支持异常处理的程序语言,检查错误需要在每个方法调用的前后加上设置和错误检查的代码,甚至每次调用同一个方法时也得如此。有了异常处理机制,可以将所有的动作都方法try块内,然后只需在一个地方就可以捕获所有的异常。这样的话,将正常执行的代码和错误检查的代码分离开来,使代码更容易编写和阅读。
12.3.2 异常处理程序
异常处理程序紧跟在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
}
每个catch子句就是一段异常处理程序,看似是只能接收一个特殊类型的参数的方法。
当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入catch子句执行,一旦执行结束,则处理过程结束。
终止与恢复
异常处理理论上有两种基本模型:终止模型和恢复模型。
Java支持终止模型:一旦异常被抛出,则无法挽回,不能返回执行。
恢复模型:异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。不过并不实用,其主要原因是:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码,增加了代码编写和维护的困难。
12.4 创建自定义异常
Java提供的异常体系不可能预见所有的错误,我们需要自定义异常类来表示程序中可能会遇到的特定问题。
自定义异常类必须继承已有的异常类:
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 exceptions = new InheritingExceptions();
try {
exceptions.f();
} catch (SimpleException e) {
System.out.println("Caught it");
}
}
}
上例的异常类只具有默认构造器,但对于异常类来说,最重要的部分就是类名,所以上例中的异常类在多数情况下是够用的。
下例是为异常类定义了一个接收字符串参数的构造器:
class MyException extends Exception {
public MyException() { super(); }
public MyException(String message) { super(message); }
}
public class FullConstructors {
public static void f() throws MyException {
System.out.println("Throw MyException from f()");
throw new MyException();
}
public static void g() throws MyException {
System.out.println("Throw 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);
}
}
}
对于第二个构造器,使用super关键字明确调用了其基类构造器,并接收一个字符串作为参数。
上例在异常程序中,调用了在Throwable类声明的printStackTrace()方法,它打印从方法调用处直到异常抛出处的方法调用序列。这里,信息被发送到了System.out。但如果调用默认版本,则信息将被输出到标准错误流System.err。
12.4.1 异常与记录日志
下面是通过日志记录异常信息:
class LoggingException extends Exception {
private static Logger logger = Logger.getLogger("LoggingException");
public LoggingException() {
StringWriter trace = new StringWriter();
printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
}
public class LoggingExceptions {
public static void main(String[] args) {
try {
throw new LoggingException();
} catch (LoggingException e) {
System.err.println("Caught " + e);
}
try {
throw new LoggingException();
} catch (Exception e) {
System.err.println("Caught " + e);
}
}
}
静态的Logger.getLogger()方法创建了一个String参数相关联的Logger对象,通常是与错误相关的包名和类名,该Logger对象会将其输出发送到System.err。
向Logger写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是severe()。
为了产生日志记录消息,我们需要获取异常抛出处的栈轨迹。printStackTrace()的重载方法接收一个java.io.PrintWriter对象作为参数,我们将一个java.io.StringWriter对象传递给PrintWriter构造器,则可以用该StringWriter对象接收栈轨迹的信息,并记录。
大多数情况下,我们需要捕获和记录其他人编写的异常,此时,必须在异常处理程序中生成日志消息:
public class LoggingExceptions2 {
private static Logger logger = Logger.getLogger("LoggingExceptions2");
static void logException(Exception e) {
StringWriter trace = new StringWriter();
e.printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
public static void main(String[] args) {
try {
throw new NullPointerException();
} catch (NullPointerException e) {
logException(e);
}
}
}
还可以更进一步自定义异常,比如加入额外的构造器和成员:
class MyException2 extends Exception {
private int x;
public MyException2() {}
public MyException2(String msg) { super(msg); }
public MyException2(String msg, int x) {
super(msg);
this.x = x;
}
public int val() { return x; }
public String getMessage() {
return "Detail Message: " + x + " " + super.getMessage();
}
}
public class ExtraFeatures {
public static void f() throws MyException2 {
System.out.println("Throw MyException2 from f()");
throw new MyException2();
}
public static void g() throws MyException2 {
System.out.println("Throw MyException2 from g()");
throw new MyException2("Originated in g()");
}
public static void h() throws MyException2 {
System.out.println("Throw MyException2 from h()");
throw new MyException2("Originated in h()" , 66);
}
public static void main(String[] args) {
try {
f();
} catch (MyException2 e) {
e.printStackTrace(System.out);
}
try {
g();
} catch (MyException2 e) {
e.printStackTrace(System.out);
}
try {
h();
} catch (MyException2 e) {
e.printStackTrace(System.out);
System.out.println("e.val() = " + e.val());
}
}
}
异常是对象的一种,所以,我们可以通过修改它以得到更强大功能的异常类。
12.5 异常说明
异常说明:Java提供的强制语法,用于告知客户端程序员某个方法可能会抛出的异常类型,使得客户端程序可以得知所调用方法中所有潜在的异常并加以处理。它属于方法声明的一部分,紧跟在形式参数列表之后。
异常说明使用了关键字throws,后面紧跟一个所有潜在异常类型列表,而RuntimeException的子类则不必声明,而在编译期被强制检查的异常称为被检查的异常:
void f() throws Exception1,Exception2 {}
如果方法里产生了异常,编译器会发现该问题并提醒:捕获并处理该异常,或在异常说明中表明此方法将产生异常。
如果方法并未产生异常,也可以在异常说明中声明异常。在定义抽象基类和接口时,这种能力就会被使用,这样使得派生类或接口实现能够抛出这些预先声明的异常。
12.6 捕获所有异常
我们可以通过捕获异常类型的基类Exception来捕获所有类型的异常:
catch (Exception e) {
System.out.println("Caught an exception");
}
应该将其放置处理程序序列表的末尾,以防止有未捕获的异常。
Exception是所有异常类的基类,它继承自Throwable,具有以下方法:
- String getMessage():获取详细信息。
- String getLocalizedMessage():获取本地语言表示的详细信息。
- String toString():对Throwable的简单描述,包含详细信息。
- void printStackTrace():将栈轨迹输出到标准错误流。
- void printStackTrace(PrintStream):将栈轨迹输出到流。
- void printStackTrace(PrintWriter):将栈轨迹输出到流。
- Throwable fillInStackTrace():Throwable对象内部记录栈帧的当前状态,用于程序重新抛出错误或异常。
此外,Throwable也是从根基类Object继承而来的,所以也具有Object的方法:
- getClass():返回一个表示此对象类型的对象。
下例演示了如何使用Exception类型的方法:
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);
}
}
}
可以发现,每个方法都比前一个提供了更多的信息。
12.6.1 栈轨迹
printStackTrace()方法所提供的信息可以通过getStackTrace()方法直接获取。该方法将返回一个由栈轨迹中的元素所构成的数组,其中每个元素都表示栈中的一帧。元素0是栈顶元素,并且是调用序列中的最后一个方法调用,即异常被创建和抛出之处。数组中的最后一个元素和栈底是调用序列的第一个方法调用。 例如:
public class WhoCalled {
static void f() {
try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement element : e.getStackTrace()) {
System.out.println(element);
}
}
}
static void g() { f(); }
static void h() { g(); }
public static void main(String[] args) {
f();
System.out.println("-----------------------");
g();
System.out.println("-----------------------");
h();
}
}
12.6.2 重新抛出异常
有时在捕获到异常后,会将刚捕获的异常重新抛出,例如:
catch (Exception e) {
System.out.println("An exception was thrown");
throw e;
}
重抛异常会把异常抛给上一级环境中的异常处理程序,并且异常对象的所有信息都会被保存。
如果将当前异常对象重新抛出,printStackTrace()方法显示的是原来异常抛出点的调用信息,而并非重新抛出点的信息。fillInStackTrace()方法则可以更新抛出点信息,并返回一个Throwable对象:
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown form 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);
}
try {
h();
} catch (Exception e) {
System.out.println("main:printStackTrace()");
e.printStackTrace(System.out);
}
}
}
我们发现,调用fillInStackTrace()的那一行变成了异常的新发生点了,并且丢失了最初异常发生点的信息,只保留了新抛出点信息。此外,如果在捕获异常后抛出另一种异常,情况也是如此:
class OneException extends Exception {
public OneException(String message) { super(message); }
}
class TwoException extends Exception {
public TwoException(String message) { super(message); }
}
public class RethrowNew {
public static void f() throws OneException {
System.out.println("Originating the exception in f()");
throw new OneException("thrown form f()");
}
public static void main(String[] args) {
try {
try {
f();
} catch (Exception e) {
System.out.println("Caught in inner try , e.printStackTrace()");
e.printStackTrace(System.out);
throw new TwoException("from inner try");
}
} catch (Exception e) {
System.out.println("Caught in outter try , e.printStackTrace()");
e.printStackTrace(System.out);
}
}
}
最外层捕获的异常,只知道自己来自于main(),对f()一无所知。
12.6.3 异常链
异常链:在捕获一个异常后抛出另一个异常,并把原始异常的信息保存下来。
JDK 1.4后,所有Throwable子类都有一个接收cause对象作为参数的构造器。该cause对象用于表示原始异常,这样使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。
在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器:
- Error:用于Java虚拟机报告系统错误。
- Exception:异常基类型。
- RuntimeException:Java标准运行时异常,编程错误。
如果要把其他类型的异常链接起来,应该使用initCause()方法。
下例演示了在运行时动态地向DynamicFields对象添加字段:
class DynamicFieldsException extends Exception {}
public class DynamicFields {
private Object[][] fields;
public DynamicFields(int initialSize) {
fields = new Object[initialSize][2];
for (int i = 0; i < initialSize; i++)
fields[i] = new Object[] { null, null };
}
public String toString() {
StringBuffer result = new StringBuffer();
for (Object[] obj : fields) {
result.append(obj[0]);
result.append(": ");
result.append(obj[1]);
result.append("\n");
}
return result.toString();
}
private int hasField(String id) {
for (int i = 0; i < fields.length; i++)
if (id.equals(fields[i][0])) {
return i;
}
return -1;
}
private int getFieldNumber(String id) throws NoSuchFieldException {
int fieldNum = hasField(id);
if (fieldNum == -1)
throw new NoSuchFieldException();
return fieldNum;
}
private int makeField(String id) {
for (int i = 0; i < fields.length; i++)
if (fields[i][0] == null) {
fields[i][0] = id;
return i;
}
// No empty fields. Add One
Object[][] tmp = new Object[fields.length + 1][2];
for (int i = 0; i < fields.length; i++)
tmp[i] = fields[i];
for (int i = fields.length; i < tmp.length; i++)
tmp[i] = new Object[] { null, null };
fields = tmp;
return makeField(id);
}
public Object getField(String id) throws NoSuchFieldException {
return fields[getFieldNumber(id)][1];
}
public Object setField(String id, Object value) throws DynamicFieldsException {
if (value == null) {
DynamicFieldsException exception = new DynamicFieldsException();
exception.initCause(new NullPointerException());
throw exception;
}
int fieldNum = hasField(id);
if (fieldNum == -1)
fieldNum = makeField(id);
Object result = null;
try {
result = getField(id);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
fields[fieldNum][1] = value;
return result;
}
public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
System.out.println(df);
try {
df.setField("d", "A Value for d");
df.setField("number", "47");
df.setField("number2", "48");
System.out.println(df);
df.setField("d", "A New Value for d");
df.setField("number3", "11");
System.out.println(df);
System.out.println(df.getField("d"));
df.setField("d", null);
} catch (NoSuchFieldException e) {
e.printStackTrace(System.out);
} catch (DynamicFieldsException e) {
e.printStackTrace(System.out);
}
}
}
我们发现,在setField()方法中抛出DynamicFieldsException异常前,先将NullPointerException对象传递给initCause()方法。而getField()方法可能会抛出NoSuchFieldException异常,我们则将其作为参数转换为RuntimeException异常。
12.7 Java标准异常
Throwable:用于表示任何可以作为异常被抛出的类。可以分为两种类型:
- Error:用于表示编译时和系统错误。
- Exception:异常的基类型。
12.7.1 特例:RuntimeException
所有RuntimeException异常会自动被Java虚拟机抛出,被称为不受检查异常,不必在异常说明中列出。这种异常属于错误,不必手动处理。如果发生这种异常,它会穿越所有的执行路径直达main()方法:
public class NeverCaught {
static void f() { throw new RuntimeException("From f()"); }
static void g() { f(); }
public static void main(String[] args) {
g();
}
}
RuntimeException类型的异常,编译器不需要异常说明,其输出会被报告给System.err:
Exception in thread "main" java.lang.RuntimeException: From f()
at NeverCaught.f(NeverCaught.java:4)
at NeverCaught.g(NeverCaught.java:5)
at NeverCaught.main(NeverCaught.java:8)
在代码中不必捕获RuntimeException类型的异常,它们代表的是编程错误:
- 无法预料的错误:如NullPointerException
- 代码中需要检查的错误:如ArrayIndexOutOfBoundsException
12.8 使用finally进行清理
finally子句中的代码,无论try块中是否有异常抛出,是否会被catch子句捕获,它们都将执行。finally子句位于异常处理程序后:
try {
// The guarded region: Dangerous activities
// that might throw A,B,or C
} catch (A a1) {
// Handler for situation A
} catch (B b1) {
// Handler for situation B
} catch (C c1) {
// Handler for situation C
} finally {
// Activities that happen every time
}
下面的程序证明了finally子句总能执行:
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;
}
}
}
}
可以发现,无论异常是否被抛出,finally子句总能被执行。
我们通过将try块放在循环中,使得程序必须达到某种条件,或尝试一定次数才能结束,从而使程序的健壮性更上一个台阶。
12.8.1 finally用来做什么
对于没有垃圾回收和析构函数自动调用机制的语言来说,finally通常用于释放内存。
在Java中,通常使用finally子句将除内存之外的资源恢复到它们的初始状态。这种资源包括:已经打开的文件或网络连接,开关等。
public class Switch {
private boolean state = false;
public boolean read() { return state; }
public void on() {
state = true;
System.out.println(this);
}
public void off() {
state = false;
System.out.println(this);
}
public String toString() {
return state ? "on" : "off";
}
}
public class OnOffSwitch {
public static Switch sw = new Switch();
public static void f() throws OnOffException1, OnOffException2 {}
public static void main(String[] args) {
try {
sw.on();
f();
} catch (OnOffException1 e) {
System.out.println("OnOffException1 e ");
} catch (OnOffException2 e) {
System.out.println("OnOffException2 e ");
} finally {
sw.off();
}
}
}
程序的目的在于确保main()结束时开关必须关闭。
12.8.2 在return中使用finally
通过使用finally,我们可以保证在方法中使用return返回时,清理工作仍旧执行:
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);
}
}
}
从输出中可以看出,在try子句内部,从何处返回都无关紧要,finally子句总会执行。
12.8.3 遗憾:异常丢失
异常作为程序出错的标志,决不应该被忽略。不过,使用finally子句时,则可以将异常忽略:
class VeryImportantException extends Exception {
public String toString() {
return "A Very Important Exception";
}
}
class HoHumException extends Exception {
public String toString() {
return "A Trivial Exception";
}
}
public class LostMessage {
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws HoHumException {
throw new HoHumException();
}
public static void main(String[] args) {
try {
LostMessage lostMessage = new LostMessage();
try {
lostMessage.f();
} finally {
lostMessage.dispose();
}
} catch (Exception e) {
System.out.println(e);
}
}
}
可以看到,VeryImportantException被忽略。下例是一种更简单的丢失异常的方式:
public class ExceptionSilencer {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
return;
}
}
}
12.9 异常的限制
当覆盖方法时,只能抛出在基类方法的异常说明中列出的那些异常。
下例演示了在编译时施加在异常上面的限制:
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class PopFoul extends Foul {}
class Strike extends BaseballException {}
class StormException extends Exception {}
class RaineOut extends StormException {}
abstract class Inning {
public Inning() throws BaseballException {}
public void event() throws BaseballException {}
public abstract void atBat() throws Strike,Foul;
public void walk() {}
}
interface Storm {
public void event() throws RaineOut;
public void rainHard() throws RaineOut;
}
public class StormyInning extends Inning implements Storm{
public StormyInning() throws RaineOut,BaseballException {}
public StormyInning(String s) throws Foul,BaseballException {}
// ! public void walk() throws PopFoul {}
public void rainHard() throws RaineOut {}
public void event() {}
public void atBat() throws PopFoul {}
public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch (PopFoul e) {
System.out.println("Pop foul");
} catch (RaineOut e) {
System.out.println("Rained out");
} catch (BaseballException e) {
System.out.println("Generic baseball exception");
}
try{
Inning inning = new StormyInning();
inning.atBat();
} catch (Strike e) {
System.out.println("Strike");
} catch (Foul e) {
System.out.println("Foul");
} catch (RaineOut e) {
System.out.println("Rained out");
} catch (BaseballException e) {
System.out.println("Generic baseball exception");
}
}
}
首先可以发现:异常限制对构造器不起作用,派生类构造器的异常说明在包含基类构造器的异常说明的基础上,还可以包含其他未被声明的异常。
其次,如果基类中的方法并未抛出异常,而其派生类所覆盖的该方法抛出异常,则无法通过编译。如果编译器允许其编译通过,那么使用多态时,程序会发生错误。
StormyInning中覆盖的event()方法表明:即使基类中的方法有抛出异常,派生类中的覆盖的方法也可以不抛出任何异常。而atBat()方法则表明:派生类中所覆盖的方法抛出的异常可以是基类方法抛出异常的子类型。 即:派生类中所覆盖的方法抛出的异常是基类中该方法所抛出异常的子集。
在main()方法中,如果是StormyInning对象调用的atBat()方法,编译器会强制捕获该类中该方法所抛出的异常,而如果是其基类,编译器则会正确地要求捕获其基类中该方法所抛出的异常。
异常说明并不属于方法类型的一部分,方法类型是由方法名和参数类型组成。因此,不能基于异常说明来重载方法。
12.10 构造器
构造器一般用于成员的初始化,下面的例子在构造器中打开一个文件:
public class InputFile {
private BufferedReader reader;
public InputFile(String fileName) throws FileNotFoundException {
try {
reader = new BufferedReader(new FileReader(fileName));
} catch (FileNotFoundException e) {
System.out.println("Could not open " + fileName);
// Wasn't open ,so don't close it
throw e;
} catch (Exception e) {
// All Other exceptions must close it
try {
reader.close();
} catch (IOException e2) {
System.out.println("reader.close() unSuccessful");
}
throw e;
} finally {
//Don't close it here!!!
}
}
public String getLine() {
String s;
try {
s = reader.readLine();
} catch (IOException e) {
throw new RuntimeException("readLine() failed");
}
return s;
}
public void dispose() {
try {
reader.close();
System.out.println("dispose() successful");
} catch (IOException e) {
throw new RuntimeException("reader.close() failed");
}
}
}
由于抛出FileNotFoundException异常时,文件未被打开,所以不能在finally子句中将文件关闭,而在捕获其他异常的catch子句中必须关闭文件。close()方法可能抛出异常,因此还需再用一层try-catch。
对于在构造阶段可能会抛出异常,并要求清理的类,最安全的方式是使用嵌套的try子句:
public class Cleanup {
public static void main(String[] args) {
try {
InputFile in = new InputFile("/Users/weiqing.jiao/Documents/workspace/ThinkInJava12/src/com/jiao/thinkInJava/test/example/constructor/Cleanup.java");
try {
String s;
int i = 1;
while ((s = in.getLine()) != null) {
System.out.println(s);
}
} catch (Exception e) {
System.out.println("Caught Exception in main");
e.printStackTrace(System.out);
} finally {
in.dispose();
}
} catch (FileNotFoundException e) {
System.out.println("InputFile construction failed");
}
}
}
使用这种方式,finally子句在构造失败时不会执行,而在构造成功后将总是执行。
这种通用的清理惯用法在构造器不抛出任何异常时也应该应用,其基本规则是:在创建需要清理的对象之后,立即进入一个try-finally语句块:
class NeedsCleanup {
private static long counter = 1;
private final long id = counter++;
public void dispose() {
System.out.println("NeedsCleanup " + id + " disposed");
}
}
class ConstructionException extends Exception {}
class NeedsCleanup2 extends NeedsCleanup {
public NeedsCleanup2() throws ConstructionException{}
}
public class CleanupIdiom {
public static void main(String[] args) {
NeedsCleanup nc1 = new NeedsCleanup();
try {
// ...
} finally {
nc1.dispose();
}
NeedsCleanup nc2 = new NeedsCleanup();
NeedsCleanup nc3 = new NeedsCleanup();
try {
// ...
} finally {
nc3.dispose();
nc2.dispose();
}
try {
NeedsCleanup2 nc4 = new NeedsCleanup2();
try {
NeedsCleanup2 nc5 = new NeedsCleanup2();
try {
// ...
} finally {
nc5.dispose();
}
} catch (ConstructionException e) {
System.out.println(e);
} finally {
nc4.dispose();
}
} catch (ConstructionException e) {
System.out.println(e);
}
}
}
对于每一个可能抛异常的构造,都必须包含在其自己的try-catch语句块中,并且每一个对象构造必须都跟随一个try-finally语句块以确保清理。
注意,如果dispose()会抛出异常,则需要额外的try语句块。即:仔细考虑所有的可能性,并确保正确处理每一种情况。
12.11 异常匹配
使用try-catch捕获异常时,异常处理系统会查找与异常匹配的的最近处理程序,并且执行完毕后,不再继续查找。查找时,并不要求抛出的异常同处理程序所声明的异常完全匹配,派生类的对象也可以匹配其基类的处理程序:
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 e) {
System.out.println("Caught Annoyance");
}
try {
throw new Sneeze();
} catch (Annoyance e) {
System.out.println("Caught Annoyance");
}
}
}
如果把捕获基类的catch子句放在最前面,编译器则会发现所有派生类的异常永远也得不到执行,从而报错:
try {
throw new Sneeze();
} catch (Annoyance e) {
System.out.println("Caught Annoyance");
} catch (Sneeze e) {
System.out.println("Caught Sneeze");
}
12.12 其他可选方式
异常系统就像一个活门,当异常情形发生时,放弃程序的正常执行序列。
我们需要注意以下异常处理的重要信息:
- 重要原则:只有知道如何处理的情况下才捕获异常。
- 重要目标:把错误处理的代码同错误发生的地点相分离。
有时,我们会无意将被检查的异常吞食了,即捕获了异常却不做任何处理,从而使得编译通过:
try {
// ... to do something useful
} catch(ObligatoryException e) {
// nothing
}
12.12.1 历史
异常处理起源于PL/1和Mesa之类的系统中,后来又出现在CLU、Smalltalk、Modula-3、Ada、Eiffel、C++、Python、java以及Ruby和C#中。
12.12.2 观点
Java发明了被检查的异常,但其有利有弊。过多的类型检查会使得大项目无法管理。关于被检查的异常和强静态类型检查,我们需要了解以下几点:
- 不在于编译器是否会强制程序员去处理错误,而是要有一致的、使用异常来报告错误的模型。
- 不在于什么时候进行检查,而是一定要有类型检查。即:必须强制程序使用正确的类型,至于这种强制施加于编译时还是运行时,无需限制。
12.12.3 把异常传递给控制台
对于简单的程序,可以直接将异常从main()方法传递到控制台:
public class MainException {
public static void main(String[] args) throws Exception {
FileInputStream file = new FileInputStream("/Users/weiqing.jiao/Documents/workspace/ThinkInJava12/src/com/jiao/thinkInJava/test/example/MainException.java");
file.close();
}
}
注意,main()方法也可以具有异常说明。
12.12.4 把被检查的异常转换为不受检查的异常
当我们虽然不知道该如何处理这个异常,但也不想将其吞食或打印一些无用消息时,异常链可以帮助解决问题,即:将被检查的异常包装进RuntimeException中:
try {
// ... to do something useful
} catch(IDontKnowWhatToDoWithThisCheckedException e) {
throw new RuntimeException(e);
}
这种方法使得我们可以不写try-catch子句和异常说明,直接忽略异常,让它自己沿着调用栈往上冒泡。并且,还可以通过getCause()捕获并处理特定的异常:
class WrapCheckedException {
void throwRuntimeException(int type) {
try {
switch (type) {
case 0: throw new FileNotFoundException();
case 1: throw new IOException();
case 2: throw new RuntimeException("Where am I?");
default: return;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class SomeOtherException extends Exception {}
public class TurnOffChecking {
public static void main(String[] args) {
WrapCheckedException wce = new WrapCheckedException();
wce.throwRuntimeException(3);
for (int i = 0; i < 4; i++)
try {
if(i < 3)
wce.throwRuntimeException(i);
else
throw new SomeOtherException();
} catch (SomeOtherException e) {
System.out.println("SomeOtherException: " + e);
} catch (RuntimeException re) {
try {
throw re.getCause();
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException: " + e);
} catch (IOException e) {
System.out.println("IOException: " + e);
} catch (Throwable e) {
System.out.println("Throwable: " + e);
}
}
}
}
12.13 异常使用指南
应该在下列情况下使用异常:
- 在知道该如何处理的情况下才捕获异常。
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化。
- 让类库和程序更安全。
12.14
异常是Java程序设计不可分割的一部分,必须熟练使用。
异常处理的优点之一是:把错误处理的代码同错误发生的地点相分离。在Java中,异常最主要的作用是报告错误。