Java编程思想之十二 通过异常处理错误
Java的基本概念是结构不佳的代码不能运行余下的问题必须在运行期间解决,这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确处理这里问题。
12.1 概念
使用异常所带来的另一个相当明显的好处,它往往能够降低错误处理代码的复杂度。
12.2 基本异常
异常情形是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常所发生的事情。
当抛出异常后,有几件事就会发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象,然后,当前的执行路径被终止,并且从当前环境中弹出堆异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继承执行程序。这个恰当的地方就是异常处理程序,它的任务就是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继承运行下去。
12,2,1 异常参数
我们总用new在堆上创建异常对象,这也伴随着存储控件的分配和构造器的调用。所有标准异常类都有两个构造器:
- 默认构造器
- 接受字符串作为参数,把相关信息放入异常对象的构造器。
使用new创建了异常对象之后,此对象的引用将传给throw。
可以简单把异常处理看成一种不同的返回机制。抛出异常的方法从当前的作用域退出。将返回一个异常对象,然后退出方法作用域。
12.3 捕获异常
异常如何被捕获,必须首先理解监控区域。
12.3.1 try块
异常处理机制,可以把所有动作都放在try块中,然后再一个地方处理捕获的异常就可以了。
12.3.2 异常处理程序
抛出的异常必须在某处得到处理。这个地点就是异常处理程序。异常处理程序紧跟在try块后,以关键字catch表示。
当异常抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。
终止与恢复
异常处理理论有两种基本模型:
- Java支持终止模型:假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行,一旦异常被抛出,就表明错误已无法挽回,也不能会来继续执行。
- 恢复模式:异常处理程序的工作就是修正错误,然后重新尝试调用出问题的放大,并认为第二次能够成功。
恢复模型会导致耦合:恢复性的处理程序需要了解异常抛出的地点,着势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难。
12.4 创建自定义异常
要自己定义异常,必须从已有的异常类继承,最好选择意思相近的异常类继承。
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!");
}
}
}
class SimpleException extends Exception
{
}
编译器创建了默认构造器,它将自动调用基类的默认构造器。
public class FullConstructors
{
public static void f() throws MyException{
System.out.println("Throwing MyExcetion from f()");
throw new MyException();
}
public static void g() throws MyException{
System.out.println("Throwing MyExcetion 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);
}
}
}
class MyException extends Exception{
public MyException(){}
public MyException(String msg){super(msg);}
}
12.4.1 异常与记录日志
使用基本日志记录功能
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
public class LoggingExceptions
{
public static void main(String[] args){
try{
throw new LoggingException();
}
catch (LoggingException e){
System.out.println("Caught "+e);
}
try{
throw new LoggingException();
}
catch (LoggingException e){
System.out.println("Caught "+e);
}
}
}
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());
}
}
需要捕获或记录他人编写的异常,必须在异常处理程序中生成日志消息
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
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);
}
}
}
更进一步自定义异常,加入额外的构造器和成员
public class ExtraFeatures
{
public static void f() throws MyException2{
System.out.println("Throwing MyException2 from f()");
throw new MyException2();
}
public static void g() throws MyException2{
System.out.println("Throwing MyException2 from g()");
throw new MyException2("Originated in g()");
}
public static void h() throws MyException2{
System.out.println("Throwing MyException2 from h()");
throw new MyException2("Originated in g()",47);
}
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());
}
}
}
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();
}
}
异常也是一种对象,所有可以继续修改异常类,获得更强大的功能。
12.5 异常说明
Java提供语法,使可以告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法说明的一部分,紧跟在形式参数之后。
异常说明使用了附加的关键字throws,后面接一个潜在的异常类型的列表,所以方法定义可能看起来像这样:
void f() throws TooBig,TooSmall
{
}
如果方法里的代码产生了异常却没有处理,编译器会发现这个问题并且提醒你,要么处理这个异常,要么就在异常说明中表明此方法将产生异常。
这种编译时被强制检查的异常称为被检查的异常。
12.6 捕获所有异常
gillInStackTrace()用于在Throwable对象的内部记录栈帧的当前状态。这在程序重新抛出错误或异常时很有用。
12.6.1 栈轨迹
printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中,每个元素表示栈中的一贞。
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();
}
}
12.6.2 重新抛出异常
重新抛出异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将被忽略。异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。
public class Rethrowing
{
public static void f() throws Exception{
System.out.println("originating the ");
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);
}
try{
h();
}catch (Exception e){
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}
有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是于新的跑出点有关的信息:
public class RethrowNew
{
public static void f() throws OneException{
System.out.println("originating the exception");
throw new OneException("thrown from f()");
}
public static void main(String[] args){
try{
try{
f();
}catch (OneException e){
System.out.println("Caught in inner try,e.printStackTrace");
e.printStackTrace(System.out);
throw new TwoException("from inner try");
}
}catch (TwoException e){
System.out.println("Caught in inner try,e.printStackTrace");
e.printStackTrace(System.out);
}
}
}
class OneException extends Exception{
public OneException(String s){super(s);}
}
class TwoException extends Exception{
public TwoException(String s){super(s);}
}
异常都是用new在堆上创建的对象,所有垃圾回收器会自动把它们清理掉。
12.6.3 异常链
想在捕获一个异常后抛出另一个异常,并且希望把原始异常信息保持下来,这被称为异常链。现在所有Throwable的子类在构造器中都可以接受一个causr对象作为参数。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链最终到最初发生的位置。
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() {
StringBuilder result = new StringBuilder();
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;
// Recursive call with expanded fields:
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) {
// Most exceptions don't have a "cause" constructor.
// In these cases you must use initCause(),
// available in all Throwable subclasses.
DynamicFieldsException dfe =
new DynamicFieldsException();
dfe.initCause(new NullPointerException());
throw dfe;
}
int fieldNumber = hasField(id);
if(fieldNumber == -1)
fieldNumber = makeField(id);
Object result = null;
try {
result = getField(id); // Get old value
} catch(NoSuchFieldException e) {
// Use constructor that takes "cause":
throw new RuntimeException(e);
}
fields[fieldNumber][1] = value;
return result;
}
public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
System.out.print(df);
try {
df.setField("d", "A value for d");
df.setField("number", 47);
df.setField("number2", 48);
System.out.print(df);
df.setField("d", "A new value for d");
df.setField("number3", 11);
System.out.print("df: " + df);
System.out.print("df.getField(\"d\") : " + df.getField("d"));
Object field = df.setField("d", null); // Exception
} catch(NoSuchFieldException e) {
e.printStackTrace(System.out);
} catch(DynamicFieldsException e) {
e.printStackTrace(System.out);
}
}
}
Java标准异常
Throwable这个Java类被用来表示任何可以作为异常抛出的类。Throwable对象可分为两中类型:Error用来表示编译时和系统错误。Exception是可以被抛出的基本类型。
RuntimeException类型的异常也许会穿越所有执行路径直达main()方法,而不会被捕获。
public class NeverCaught
{
static void f(){
throw new RuntimeException("From f()");
}
static void g(){
f();
}
public static void main(String[] args){
g();
}
}
RuntimeException代表的是编程错误:
- 无法预料的错误
- 作为程序员,应该在代码中检查的错误
应该把异常机制用来处理一些烦人的运行时错误,这些错误往往是由代码控制能力之外的因素导致的。
12.8 使用finally进行清理
对于一些代码,可能会希望无论try中的异常是否抛出,都能得到执行。可以使用finally子句。
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("finally cluse "+count);
if(count==2)break;
}
}
}
}
class ThreeException extends Exception{}
12.8.1 finally用来做什么
当要把除内存之外的资源恢复到初始状态时,就要用到finally子句
public class WithFinally
{
static Switch sw = new Switch();
public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
OnOffSwitch.f();
} catch(OnOffException1 e) {
System.out.println("OnOffException1");
} catch(OnOffException2 e) {
System.out.println("OnOffException2");
} finally {
sw.off();
}
}
}
class Switch {
private boolean state = false;
public boolean read() { return state; }
public void on() { state = true; System.out.print(this); }
public void off() { state = false; System.out.print(this); }
public String toString() { return state ? "on" : "off"; }
}
class OnOffSwitch {
private static Switch sw = new Switch();
public static void f()
throws OnOffException1,OnOffException2 {}
public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
f();
sw.off();
} catch(OnOffException1 e) {
System.out.println("OnOffException1");
sw.off();
} catch(OnOffException2 e) {
System.out.println("OnOffException2");
sw.off();
}
}
}
class OnOffException1 extends Exception {}
class OnOffException2 extends Exception {}
当设计break和continue语句时,finally也会得到执行
12.8.2 在return中使用finally
因为finally子句总会执行,所以在一个方法中,可以从多个点返回,并且可以保证重要的清理工作仍旧会执行:
public class MultipleReturns
{
public static void f(int i){
System.out.println("Initialization that requires");
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;
}
finally
{
System.out.println("end");
}
}
public static void main(String[] args){
for(int i=1;i<=3;i++)
f(i);
}
}
12.8.3 遗憾:异常丢失
public class LoseMessage
{
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws HoHumException {
throw new HoHumException();
}
public static void main(String[] args) {
try {
LoseMessage lm = new LoseMessage();
try {
lm.f();
} finally {
lm.dispose();
}
} catch(Exception e) {
System.out.println(e);
}
}
}
class VeryImportantException extends Exception {
public String toString() {
return "A very important exception!";
}
}
class HoHumException extends Exception {
public String toString() {
return "A trivial exception";
}
}
前一个异常还没有处理就抛出了下一个异常。
12.9 异常的限制
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
abstract class Inning {
public Inning() throws BaseballException {}
public void event() throws BaseballException {
// Doesn't actually have to throw anything
}
public abstract void atBat() throws Strike, Foul;
public void walk() {} // Throws no checked exceptions
}
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
interface Storm {
public void event() throws RainedOut;
public void rainHard() throws RainedOut;
}
public class StormyInning extends Inning implements Storm {
// OK to add new exceptions for constructors, but you
// must deal with the base constructor exceptions:
public StormyInning()
throws RainedOut, BaseballException {}
public StormyInning(String s)
throws Foul, BaseballException {}
// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
// Interface CANNOT add exceptions to existing
// methods from the base class:
//! public void event() throws RainedOut {}
// If the method doesn't already exist in the
// base class, the exception is OK:
public void rainHard() throws RainedOut {}
// You can choose to not throw any exceptions,
// even if the base version does:
public void event() {}
// Overridden methods can throw inherited exceptions:
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(RainedOut e) {
System.out.println("Rained out");
} catch(BaseballException e) {
System.out.println("Generic baseball exception");
}
// Strike not thrown in derived version.
try {
// What happens if you upcast?
Inning i = new StormyInning();
i.atBat();
// You must catch the exceptions from the
// base-class version of the method:
} catch(Strike e) {
System.out.println("Strike");
} catch(Foul e) {
System.out.println("Foul");
} catch(RainedOut e) {
System.out.println("Rained out");
} catch(BaseballException e) {
System.out.println("Generic baseball exception");
}
}
}
派生类构造器不能捕获基类构造器抛出的异常。
派生类可以不抛出任何异常,即使它是基类定义的异常。
如果处理的刚好是派生类对象的话,编译器只会强制要求你捕获这个类所抛出的异常,如果将它向上转型成基类,那么编译器就会要求你捕获基类的异常。
在继承中,基类的方法必须出现在派生类里,在继承和覆盖的过程中,某个特定的方法异常说明接口不是变大而是变小——这恰好和类接口在继承时的情形相反。
12.10 构造器
一般在构造器中,会把对象设置成安全的初始状态。如果在构造器内抛出异常,这些清理行为也许就不能正常工作了。就算使用finally每次都执行了清理代码,如果构造器在器执行的中途异常,那么某些对象就没有被创建成功,而这些部分在finally子句中却要被清理。
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class InputFile
{
private BufferedReader in;
public InputFile(String fname) throws Exception
{
try
{
in = new BufferedReader(new FileReader(fname));
// Other code that might throw exceptions
}
catch (FileNotFoundException e)
{
System.out.println("Could not open " + fname);
// Wasn't open, so don't close it
throw e;
}
catch (Exception e)
{
// All other exceptions must close it
try
{
in.close();
}
catch (IOException e2)
{
System.out.println("in.close() unsuccessful");
}
throw e; // Rethrow
}
finally
{
// Don't close it here!!!
}
}
public String getLine()
{
String s;
try
{
s = in.readLine();
}
catch (IOException e)
{
throw new RuntimeException("readLine() failed");
}
return s;
}
public void dispose()
{
try
{
in.close();
System.out.println("dispose() successful");
}
catch (IOException e2)
{
throw new RuntimeException("in.close() failed");
}
}
}
对于构造阶段可能会抛出的异常,并且要求清理的类,最安全的时使用嵌套try子句:
public class Cleanup
{
public static void main(String[] args)
{
try
{
InputFile in = new InputFile("Cleanup.java");
try
{
String s;
int i = 1;
while ((s = in.getLine()) != null)
; // Perform line-by-line processing here...
}
catch (Exception e)
{
System.out.println("Caught Exception in main");
e.printStackTrace(System.out);
}
finally
{
in.dispose();
}
}
catch (Exception e)
{
System.out.println("InputFile construction failed");
}
}
}
对InputFile对象的构造在其自己的try语句块中有效,如果构造失败,将进入外部的catch子句,而dispose()方法不会被调用。如果构造成功,我们肯定要确保对象能够被清理,因此在构造器之后立即创建一个新的try语句。执行清理的finally与内部热try语句块相关联。这种方式中,finally子句在构造失败时不会执行的,而是构造成功时才会被执行。
这种通用的清理惯用法在构造器不抛出任何异常时也应该运用。基本规则是:在创建需要清理的对象之后,立即进入一个try-finally语句中:
class NeedsCleanup
{ // Construction can't fail
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
{
// Construction can fail:
public NeedsCleanup2() throws ConstructionException
{
}
}
public class CleanupIdiom
{
public static void main(String[] args)
{
// Section 1:
NeedsCleanup nc1 = new NeedsCleanup();
try
{
// ...
}
finally
{
nc1.dispose();
}
// Section 2:
// If construction cannot fail you can group objects:
NeedsCleanup nc2 = new NeedsCleanup();
NeedsCleanup nc3 = new NeedsCleanup();
try
{
// ...
}
finally
{
nc3.dispose(); // Reverse order of construction
nc2.dispose();
}
// Section 3:
// If construction can fail you must guard each one:
try
{
NeedsCleanup2 nc4 = new NeedsCleanup2();
try
{
NeedsCleanup2 nc5 = new NeedsCleanup2();
try
{
// ...
}
finally
{
nc5.dispose();
}
}
catch (ConstructionException e)
{ // nc5 constructor
System.out.println(e);
}
finally
{
nc4.dispose();
}
}
catch (ConstructionException e)
{ // nc4 constructor
System.out.println(e);
}
}
}
Section1相当简单:遵循了在可去除对象之后紧跟try—finally原则。如果对象构造不能失败,就不需要任何catch。在Section2中,为了构造和清理,可以看到具有不能失败的构造器对象可以群组在一起。
Section3展示了如何处理那些具有可以失败的构造器,且需要清理的对象。
异常匹配
抛出异常的时候,异常处理系统会按照代码的书写顺序找出最近的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不在继续查找。
如果把捕获基类的catch子句放在最前面,以此想把派生类的异常全屏蔽,就像这样:
try
{
throw new Sneeze();
}
catch(Annoance a){}
catch(Sneeze s){}
这样编译器就会发现Sneeze的catch子句永远也得不到执行,因此它会向你报错。
12.12 其他可选方式
异常处理系统就像一个活门,使你能放弃程序的正常执行序列。开发异常处理的初衷是为了方便处理错误。
异常处理的一个重要原则:只有在你知道如何处理的情况下才捕获异常。就是把错误的代码同错误发生地点相分离。
被检查的异常:因为它们强制你在可能还没准备好处理错误的时候被迫加上catch子句,这就导致了吞食则有害的问题。
12.12.1 把异常传递给控制台
12.13 异常使用指南
应在下列情况下使用异常:
- 在恰当的级别处理问题。(知道如何处理的情况下捕获异常)
- 解决问题并且重新调用产生异常的方法
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事情尽量做完,然后把不同的异常重抛到更高层。
- 终止程序
- 进行简化。
- 让类库和程序更安全。