(1)认识异常
class MyException extends Exception{
public MyException(){}
public 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);
}
}
}
通过继承Exception对象或其子类,可以实现自己定义的异常类,最好选择意思相近的异常类继承。
本例的结果被打印到了控制台上,本书的输出显示正是在控制台上自动地捕获和测试这些结果的。但是,通过写入System.err而将错误发送给标准错误流,也许会更好,因为System.out会被重定向,而System.err不会,这样更容易被用户注意到。
使用java.util.logging工具将输出记录到日志中也是一种方法。
/*
* P253
*/
import java.util.logging.Logger;
import java.io.*;
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(LoggingException e){
System.err.println("Caught " +e);
}
}
}
要认识到,异常也是对象的一种,所以可以继续修改这个异常类,以得到更强的功能。但要记住,使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其他的就不管了(大多数java库里的异常都是这么用的,所以对异常所添加的其他功能也许根本用不上。)
(2)捕获异常
/*
* P258-259
*/
public class Rethrowing {
public static void f() throws Exception{
System.out.println("Origination 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.prinStackTrace()");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception{
try{
f();
}catch(Exception e){
System.out.println("Inside h(),e.prinStackTrace()");
e.printStackTrace(System.out);
throw (Exception)e.fillInStackTrace();
}
}
public static void main(String[] args){
try{
g();
}catch(Exception e){
System.out.println("main: getStackTrace()");
for(StackTraceElement ste: e.getStackTrace())
System.out.println(ste.getMethodName());
}
try{
h();
}catch(Exception e){
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}
在本例中,通过调用getStackTrace()方法来直接访问printStackTrace()方法。这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每个元素都表示栈中的一桢。元素0是栈顶元素,并且是调用序列中的最后一个方法调用(这个Throwable)被创建和抛出之处。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。
如果只是把当前异常对象重新抛出,那么printStackTrack()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInstackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。
永远不必为清理前一人异常对象而担心,或者说为异常对象的清理而担心。它们都是用new在堆上创建的对象,所以垃圾回收器会自动把它们清理掉。
在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。现在所有Throwable的子类在构造器中都可以接受一个cause(因由)对象作为参数来把原始异常传递给新异常,来构成异常链。在Throwable的子类中,只有三种基本的异常类提供了带参数的构造器。它们是Error,Exception以及RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause()方法面不是构造器。
/*
* P260-262
*/
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 claa 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.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("number", 11);
System.out.println("df: "+df);
System.out.println("df.getField(\"d\") : "+ df.getFieldNumber("d"));
Object fieldObject=df.setField("d", null);//Exception
}catch(NoSuchFieldException e){
e.printStackTrace(System.out);
}catch(DynamicFieldsException e){
e.printStackTrace(System.out);
}
}
}
(3)使用finally
finally可以保证无论是否有异常抛出,无论在何处返回函数,finally块中的代码总能得到执行。
对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它能使程序员保证:无论try块里发生了什么,内存总能得到释放。
但是java有垃圾回收机制,java在什么情况下才需要用到finally呢?当要把除内存之外的资源恢复到它们的初始状态时,需要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。
public class AlwaysFinally {
public static void main(String[] argw){
System.out.println("Entering first try block");
try{
System.out.println("Entering second try block");
try{
throw new FourException();
}finally{
System.out.println("finally in 2 nd try block");
}
}catch(FourException e){
System.out.println("Cayght FourException in 1st try block");
return ;
}finally{
System.out.println("finally in 1st try block");
}
}
}
运行结果:
Entering first try block
Entering second try block
finally in 2 nd try block
Cayght FourException in 1st try block
finally in 1st try block
由本例可以看出,如果异常没有得到处理,虽然finally子句会得到执行,但异常会继续向更高一层的异常处理程序抛出。
Entering second try block
finally in 2 nd try block
Cayght FourException in 1st try block
finally in 1st try block
由本例可以看出,如果异常没有得到处理,虽然finally子句会得到执行,但异常会继续向更高一层的异常处理程序抛出。
而即使在catch语句中,已经有了return 语句,之后的finally还是会得到执行。
当涉及break和continue语句的时候,finally子句还是会得到执行,把finally和break及continue配合使用,在Java里就没必要使用goto语句了。
但是,在使用finally语句的过程中,会出现异常丢失的情况:1)异常还未得到处理,进入finally抛出新的异常,原异常被新的异常取代。2)在异常还未得到处理时,进入finally并直接返回,原异常直接丢失。但实际上,异常往往会以比这两种情况更微妙更难以察觉的方式完全丢失。
(4)异常的限制
当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,尖基类使用的代码应用到其派生类对象的时候,一样能够工作。
/*
* P269-271
*/
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 exception for coustructors ,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 Erroe
//Interface cannot add exceptions to existing
//methods from the base class:
//public void event() throws RainedOut{}
//public void event() throws BaseballException{}
//You can choose to not throw any exceptions,
//even if th e base version does:
public void event(){};//event()实际也无法抛出任何异常,因为无论抛出何种类型的异常,要不与基类冲突,要不与接口冲突
//if the method doesn't already exist in the
//base class,the exception is ok;
public void rainHard() throws RainedOut{}
//Overridden methods can throw inhrited 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");
}
}
}
从这个例子可以看出,一个出现在基类方法的异常说明中异常,不一定出现在派生类方法的异常说明里,但是,一般情况下,出现在派生类方法异常说明里的异常,一定是在基类方法中已说明的异常。这点同继承的规则明显不同。在继承中,基类的方法必须出现在派生类里,换句话说,在继承和覆盖的过程中某个特定方法的“异常说明的接口”不是变大了而是变小了,这恰好和类接口继承时的情形相反。
不过异常限制对构造器不起作用。构造器可以抛出任何异常,不必理会基类构造器所抛出的异常。然而,因为基类构造器必须以这样或那样的方式被调用(这时的默认构造器将自动被调用)。派生类构造器异常说明必须包含基类构造器的异常说明。
派生类构造器不能捕获基类构造器抛出的异常。
最后一个值得注意的地方是转型。在例中可以看到,如果处理的刚好是StormyInning对象的话,编译器只会强制要求你捕获这个类所抛出的异常。但是如果将它向上转型成基类型,那么编译器就会(正确地)要求你捕获基类异常。所有这些限制都是为了能产生更为强壮的异常处理代码。
//Guaranteeing proper cleanup of a resource
import java.io.*;
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!!!
//由于finally会在每次完成构造器之后都执行一遍,因此此处不该调用close()关闭文件的地方。我们希望文件在InputFile对象的整个生命周期内都处于打开状态。
}
}
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 e){
throw new RuntimeException("in.close() failed");
}
}
}
/*
* P273
*/
public class Cleanup {
public static void main(String[] args){
try{
InputFile in=new InputFile("src/exceptions/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();
//System.out.println("the secone dipose,what will happen?");
//in.dispose();
//在此处多调用了一次in.dispose(),程序并未发生错误。
//此处写此代码的原因是为了测试程序可能在InputFile方法中已经调用一次close方法了,如果再调用一次colse()方法程序是否后报异常
}
}catch(Exception e){
System.out.println("InputFile construction failed");
}
}
}
以上两段代码展示了在发生异常后正常清理其它资源的方式。执行清理的finally与内部的try语句块相关联。在这种方式中,finally子句在构造失败时是不会执行的,而在构成成功时将总是执行。其基本规则是:在创建需要清理的对象之后,立即进入一个try-finally语句块。尽力创建不能失败的构造器(虽然这么做并非总是可行)。
(5)异常匹配
抛出异常的时候,异常处理系统会按照代码书写顺序找出最近的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。
查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。所以,应该尽量将派生类异常处理程序写有前面,而将基类异常程序写在后面,避免基类异常程序处理掉它不该处理的派生类异常。
(6)其它可选方式
“被检查的异常”使这个问题变得有些复杂。因为它们强制你在可能还没准备好处理错误的时候被迫加上catch子句,这就导致了吞食则有害(harmful if swallowed)问题。
程序员们只做最简单的事情,常常是在无意中吞食了异常,然而一旦这么做,虽然能通过编译,但除非你记得复查并改正代码,否则异常将会丢失。
异常说明可能有两种意思:一个是“我的代码会产生异常,这由你来处理”。另一个是“我的代码忽略了这些异常,这由你来处理”。学习异常处理机制和语法的时候,我们一直在关注你来处理部分,但这里特别值得注意的事实是,我们通常都忽略了异常说明所表达的完整含义。
“被检查的异常”和强静态类型检查对于开发健壮的程序曾被认为是非常有必要的。但实际这些好处来自于:
1)不在于编译器是否会强制程序员去处理错误,而是要有一致的。使用异常报告错误的模型。
2)不在于什么时候进行检查,而是一定要有类型检查,也就是说,必须强制程序使用正确的类型,至于这种强制施加于编译时还是运行时,反倒没有关系。
一种有效的方法是把“被检查的异常”包装进RuntimeException里面。这种技巧给你一种选择,你可以不写try-catch子句和/或异常说明,直接忽略异常,让它自己沿着调用栈往上冒泡。同时,还可以用getCause()捕获并处理特定的异常。
import java.io.*;
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();
//You can call throw RuntimeException() without a try block,
//and let RuntimeExceptions leave the method;
wce.throwRuntimeException(3);
// Or you can choose to catch exception;
for(int i=0;i<=3;i++){
try{
if(i<3)
wce.throwRuntimeException(i);
else throw new SomeOtherException();
}catch(SomeOtherException e){
System.out.println("SomeOtherException e"+ 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);
}
}
}
}
}
(7)异常使用指南
1)在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
2)解决问题并且重新调用产生异常的方法。
3)进行少许修补,然后绕过异常发生的地方继续执行。
4)用别的数据进行计算,以代替方法预计会返回的值。
5)把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
6)把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
7)终止程序。
8)进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)
9)让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。