概念
- 异常,这个词有有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是当前的环境还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,在这里将作出正确的决定。
- 好处:降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。
- 这种方法不仅节省代码,而且把“描述在正常执行过程中做什么事”和“出了问题怎么办”的代码相分离。
基本异常
- 异常情形:是指阻止当前方法或作用域继续执行的问题
- 抛出异常后,会有几件事随之发生:
– 同Java中其他对象的创建一样,将使用new
在堆上创建异常对象。
– 当前执行路径被终止,并且从当前环境中弹出对异常对象的引用。
– 异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。 - 抛出一个异常,把错误信息传播到更大的环境中
- 标准异常类都有两个构造器:一个是默认构造器,另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器
if(t == null) throw new NullPointerException(); /*out Exception in thread "main" java.lang.NullPointerException at chapter12.ThrowTest.f(ThrowTest.java:15) at chapter12.ThrowTest.main(ThrowTest.java:12) */ if(t == null) throw new NullPointerException("发生空指针错误"); /*out Exception in thread "main" java.lang.NullPointerException: 发生空指针错误 at chapter12.ThrowTest.f(ThrowTest.java:15) at chapter12.ThrowTest.main(ThrowTest.java:12) */
- 能够抛出任意类型的
Throwable
对象,它是异常类型的根类。通常对于不同类型的错误,要抛出相应的异常。Exception
也是Throwable
的子类。
捕获异常
try
块
如果在方法内部抛出了异常,这个方法将在抛出异常的 过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。try { // 可能产生异常的代码 }
- 异常处理程序
针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在try
块之后,以关键字catch
表示
每个try { // 可能产生异常的代码 } catch (Type1 id1) { // 处理Type1类型的异常 } catch (Type2 id2) { // 处理Type2类型的异常 } catch (Type3 id3) { // 处理Type3类型的异常 }
catch
子句,看起来就像是接收一个且近接收一个特殊类型的参数的方法。
异常处理程序必须紧跟在try
块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入catch
执行,此时认为异常得到了处理。 - 终止与恢复:异常处理理论上有两种模型:终止模型和恢复模型。
– 终止模型(Java支持的模型):将假设错误非常关键,以至于无法返回异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。
– 恢复模型:意思是异常处理程序的工作是修正错误,然后尝试调用除问题的方法,并认为第二次能成功。
创建自定义异常
- 要定义异常,必须从已有的异常类继承,最好是选择意思相近的异常类继承
// 继承异常 class SimpleException extends Exception { } // 抛出异常 public class InheritingException { public void f() throws SimpleException { throw new SimpleException(); } }
- 将异常结果打印到控制台,可以用异常自带的
printStackTrace()
方法,将异常打印到指定地方。try { tryCatchTest.throwsArithmeticException(); } catch (ArithmeticException e) { e.printStackTrace(); // 默认打印到System.err,内部实际上就是`printStackTrace(System.err);` e.printStackTrace(System.out); // 可选择自己输入一个PrintStream,打印到指定地方。 }
- 异常与记录日志
– 使用java.util.logging
工具将输出记录到日志中
– 静态的Logger.getLogger()
创建了一个String
参数相关联的Logger
对象,这个Logger
对象会将其输出发送到System.err
– 向Logger写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是servere()
– 为了获取异常消息的字符串,在printStackTrace()
方法中传入一个java.io.PrintWriter
对象作为参数,再将java.io.StringWriter
对象传递给java.io.PrintWriter
的构造器。那么,调用toString()
就可以将输出抽取为一个String
public class ExceptionLoggerTest { public static void main(String[] args) { try { throw new LoggingException(); } catch (LoggingException e) { System.err.println("Caught" + e); } } } class LoggingException extends Exception { private static Logger logger = Logger.getLogger("LoggingException"); public LoggingException() { // StringWriter将包含堆栈信息 StringWriter trace = new StringWriter(); //必须将StringWriter封装成PrintWriter对象, //以满足printStackTrace的要求 printStackTrace(new PrintWriter(trace)); logger.severe(trace.toString()); } }
Throwable
有getMessage()
方法,类似于toString()
方法,在出现异常的时候,输出到输出台。可以在继承异常的时候,重写此方法,显示更详细的内容。public String getMessage() { return "Detail Message: " + super.getMeaasge(); }
异常说明
- 使你能以礼貌地方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理,这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。
- 异常说明使用了
throws
,后面接一个所有潜在异常类型的列表,所以方法定义看起来像这样void f() throws TooBig, TooSmall, DivZero { }
- 代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器将会发现这个问题并提醒你,要么处理这个异常,要么就在异常说明中表明此方法将产生异常。
- 可以声明方法将抛出异常,实际上却在方法内没有抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占一个位置,以后就可以抛出这种异常而不需要修改已有的代码。在定义抽象基类和接口时,这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。
捕获所有异常
- 捕获异常类型的基类
Exception
catch(Exception e) { System.out.println("caught an exception"); }
Exception
继承了Throwable
,因此可以调用基类Throwable
的一些方法。
–String getMessage()
:获取异常信息
–String getLocalizedMessage()
:获取详细信息,或用本地语言表示的详细信息
–String toString()
–void printStackTrace()
、void printStackTrace(PrintStream)
、void printStackTrace(java.io.PrintWriter)
打印调用栈轨迹,调用栈显示了“把你带到了异常抛出地点”的方法调用序列。
–Throwable fillInStackTrace()
用于在Throwable
对象的内部记录栈帧的当前状态。这在程序重新抛出异常或错误时非常有用。- 栈轨迹
printStackTrace()
方法所提供的信息可以通过getStackTrace()
方法来直接访问。这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一帧。元素0时栈顶元素,并且是调用序列中的最后一个方法调用。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。public class StackTraceTest { // 一个抛出异常1的方法 public static void f() throws OneException { System.out.println("originating the exception in f()"); throw new OneException("throw from f()"); } public static void main(String[] args) { try { // 外层try 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 outer try, e.printStackTrace()"); e.printStackTrace(System.out); } } } // 异常1 class OneException extends Exception { public OneException(String message) { super(message); } } // 异常2 class TwoException extends Exception { public TwoException(String message) { super(message); } } /* out originating the exception in f() Caught in inner try, e.printStackTrace() chapter12.OneException: throw from f() at chapter12.StackTraceTest.f(StackTraceTest.java:13) // 发生异常的第一层 at chapter12.StackTraceTest.main(StackTraceTest.java:19) // 第二层 Caught in outer try, e.printStackTrace() chapter12.TwoException: from inner try at chapter12.StackTraceTest.main(StackTraceTest.java:23) // 在try里面抛出的异常 */
- 异常链:常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链
–Throwable
的子类在构造器中都可以接收一个cause
对象作为参数。这个cause
就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。
– 只有三个基本的异常类提供了cause
参数的构造器:Error
(用于Java虚拟机报告系统错误 )、Exception
以及RuntimeException
。如果其他类型的异常需要链接起来,应该使用initCause()
方法。DynamicFieldsException dfe = new DynamicFieldsException(); dfe.initCause(new NullPointerException()); throw dfe;
catch(NoSuchFieldException e) { throw new RuntimeException(e); }
Java标准异常
- Java程序员关心的基类型通常是
Exception
- 特例:
RuntimeException
– 如果对null
进行引用,Java会自动抛出NullPointerException
异常
– 属于运行时异常有很多,它们会自动被Java虚拟机抛出,所以不必在异常说明中把它们列出。
– 任何从RuntimeException
继承来的异常,都属于“不受检查异常”,不需要再异常说明中声明。这种异常属于错误,将被自动捕获。
– 如果不捕获这种类型的异常,异常会直达main()
方法
– 如果RuntimeException
没有被捕获而直达main()
,那么程序退出前将调用异常的printStackTrace()
方法。
– 只能在代码中忽略RuntimeException
类型的异常,其他类型异常的处理都是由编译器强制实施的。RuntimeException
代表的是编程错误。public class RuntimeExceptionTest { public static void f() { throw new RuntimeException(); } public static void g() { f(); } public static void main(String[] args) { g(); } } /*out Exception in thread "main" java.lang.RuntimeException at chapter12.RuntimeExceptionTest.f(RuntimeExceptionTest.java:12) at chapter12.RuntimeExceptionTest.g(RuntimeExceptionTest.java:16) at chapter12.RuntimeExceptionTest.main(RuntimeExceptionTest.java:20) */
使用finally进行清理
- 有一些代码,可能会希望无论
try
块中的异常是否抛出,它们都能得到执行。为了这种效果,可以在异常处理程序后面加上finally
子句try { // 可能抛出异常的代码 } catch(A a1) { // 处理异常的代码 } finally{ // 无论是否有错误,都会执行的代码 }
- 当要把除内存之外的资源恢复到它们的初始状态时,就要用到
finally
子句。这种需要清理的资源包括,已经打开的文件或网络连接,在屏幕上画的图形,甚至是外部世界的某个开关。 - 在异常没有被当前的异常处理程序捕获的情况下,异常处理机制也会在跳到更高的异常处理程序之前,执行
finally
子句public static void main(String[] args) { try { try { throw new RuntimeException(); } finally { System.out.println("第一层finally"); } } catch (RuntimeException e) { System.out.println("捕获" + e); } finally { System.out.println("第二层finally"); } } /* out 第一层finally 捕获java.lang.RuntimeException 第二层finally */
- 在
return
中使用finally
,try
或catch
里面return
,都要先执行finally
,再return
public static void main(String[] args) { try { throw new RuntimeException(); } catch (Exception e) { System.out.println(e); return; } finally { System.out.println("finally"); } } /* out java.lang.RuntimeException finally */
- 异常丢失
以某种特殊的方式使用finally
,就会发生忽略某些异常的情况。@Test public void test2() { try { try { throw new OneException("throw new OneException()"); // 这个异常被忽略了 } finally { throw new TwoException("throw new TwoException()"); } } catch (Exception e) { e.printStackTrace(); } } /* output chapter12.TwoException: throw new TwoException() */
@Test public void test() { try { throw new OneException("throw new OneException()"); // 异常被忽视 } finally { return; } } // 没有任何输出
异常的限制
— 当覆盖方法的时候哦,只能抛出在基类方法的异常说明里列出的那些异常。派生类方法可以缺少基类方法抛出的异常,但不能添加;可以选择抛出基类方法抛出的异常的派生异常。这一切都是为了代码的兼容性。
– 异常限制对构造器不起作用。派生类构造器可以抛出任何异常,不必理会基类构造器所抛出的异常。但是,派生类构造器的异常说明必须包含基类构造器的异常说明。
– 通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证
– 在继承和覆盖的过程中,某个特定方法的“异常说明的接口”不是变大了而是变小了。这恰好和类接口在继承时的情形相反。
构造器
- 有一点非常重要,即你要时刻询问自己,“如果异常发生了,所有东西能被正确的清理吗”
- 例子1:
– 如果FileReader
构造器失败了,将抛出FileNotFoundException
,对于这个异常,并不需要关闭文件,因为还没被打开
– 对于其他异常,由于文件已经被打开,因此必须被关闭
– 若成功打开了,in
是可以供后面的方法使用的,因此不能将close()
放在finally中,否则无论打开成功与否,都会被关闭
–close()
也会失败,因此需要再包一层try-catch
– 处理完异常之后,需要将异常重新抛出,因为防止后续以为文件打开成功,对文件进行使用。异常处理的目的仅仅是关闭文件而已。public class InputFile { private BufferedReader in; public InputFile(String name) throws Exception { try { in = new BufferedReader(new FileReader(name)); } catch (FileNotFoundException e) { // 文件找不到,即没有被打开,因此不需要关闭 System.out.println("Could not open " + name); throw e; } catch (Exception e) { try { in.close(); } catch (IOException ioException) { System.out.println("文件关闭失败"); } throw e; } finally { // 不要在此关闭文件! } } }
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("关闭成功"); } catch (IOException e) { throw new RuntimeException("in.close() failed"); } }
- 例子2:
– 当文件打开失败的时候,直接进入第一个catch
,dispose()
不会被调用
– 在使用完成之后,在finally
中,关闭打开的文件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) ; } catch (Exception e) { System.out.println("捕获异常"); } finally { in.dispose(); } } catch (Exception e) { System.out.println("文件开启失败"); } } }
异常匹配
- 异常处理系统会按照代码的书写顺序找出最近的处理程序,找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找了
- 基类异常可以捕获它及它派生的异常
public class Human { public static void main(String[] args) { try { throw new Sneeze(); } catch (Sneeze sneeze) { sneeze.printStackTrace(); } catch (Annoyance annoyance) { // 无法到达 annoyance.printStackTrace(); } try { throw new Sneeze(); } catch (Annoyance annoyance) { annoyance.printStackTrace(); } } } class Annoyance extends Exception { } class Sneeze extends Annoyance { }