一. 异常的引入及基础
发现错误的理想时机是在编译阶段。也就是在你试图运行程序之前。
然而,编译期间编译器并不能找出全部的错误,余下的错误仅仅有在运行期才干发现和解决,这类错误就是 Throwable。
这就须要错误源能够通过某种方式,把适当的信息传递给某个接收者。该接收者将知道怎样正确的处理这个问题,这就是Java的错误报告机制 —— 异常机制。该机制使得程序把 在正常运行过程中做什么事的代码 与 出了问题怎么办的代码 相分离。
在对异常的处理方面。Java 採用的是 终止模型 。在这样的模型中,将假设错误非常关键。以至于程序无法返回到异常发生的地方继续运行。一旦异常被抛出,就表明错误已经无法挽回。也不能回来继续运行。相对于终止模型,还有一种异常处理模型为 恢复模型。它使异常被处理之后能够继续运行程序。尽管该模型非常吸引人,但不是非常实用,其主要原因是它所导致的耦合:恢复性处理程序须要了解异常的抛出地点。这势必要包括依赖于抛出位置的非通用代码,从而大大添加了代码编写和维护的难度。
在异常情形中,异常的抛出伴随着以下三件事的发生:- 首先,同 Java 中其它对象的创建一样,将使用 new 在堆上创建异常对象。
- 其次,当前的运行路径被终止 。而且从当前环境中弹出对异常对象的引用。
- 最后,异常处理机制接管程序 ,并開始寻找相应的异常处理程序,并将程序从错误状态中恢复。
二. Java 标准异常
1.基本概念
异常类层次结构演示样例 - Throwable:全部的异常类型的根类
在 Java 中。Throwable 是全部的异常类型的根类。Throwable 有两个直接子类:Exception 和 Error。二者都是 Java 异常处理的重要子类,各自都包括大量子类。
- Error:程序本身无法处理的错误Error 是程序无法处理的错误,表示运行应用程序中较严重问题。这些错误大部分与代码编写者运行的操作无关。而与代码运行时的 JVM 、资源等有关。比如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续运行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时。Java虚拟机(JVM)通常会选择线程终止。这些错误是不可查的,而且它们在应用程序的控制和处理能力之外。在 Java 中。错误通过Error的子类描写叙述。
Exception:程序本身能够处理的错误
Exception 一般是Java程序猿所关心的。其在Java类库、用户方法及运行时故障中都可能抛出。它由两个分支组成: 运行时异常(派生于 RuntimeException 的异常) 和 其它异常 。划分这两种异常的规则是:由程序错误(一般是逻辑错误。如错误的类型转换、数组越界等。应该避免)导致的异常属于RuntimeException;而程序本身没有问题。但由于诸如I/O这类错误(eg:试图打开一个不存在的文件)导致的异常就属于其它异常。
此外,Java的异常(包括Exception和Error)通常可分为 受检查的异常(checked exceptions) 和 不受检查的异常(unchecked exceptions) 两种类型。
不受检查异常:派生于 Error 或 RuntimeException 的全部异常
不可查异常是编译器不要求强制处理的异常,包括运行时异常(RuntimeException与其子类)和错误(Error)。
也就是说,当程序中可能出现这类异常,即使没实用try-catch语句捕获它,也没实用throws子句声明抛出它。也会编译通过。
受检查异常:除去不受检查异常的全部异常
受检查异常是编译器要求必须处理的异常。
这里所指的处理方式有两种: 捕获并处理异常 和声明抛出异常 。也就是说。当程序中可能出现这类异常,要么用 try-catch 语句捕获它,要么用 throws 子句声明抛出它,否则编译不会通过。
准则:假设程序出现RuntimeException异常,那么一定是程序猿的问题
异常和错误的差别:异常能被程序本身处理。错误则无法处理
三. Java 异常处理机制
异常处理
在 Java 应用程序中。异常处理机制为:抛出异常 与 捕捉异常。
抛出异常: 当一个方法出现错误引发异常时。方法创建异常对象并交付运行时系统,异常对象中包括了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处理异常的代码并运行。捕获异常: 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时。即为合适 的异常处理器。运行时系统从发生异常的方法開始,依次回查调用栈中的方法。直至找到含有合适异常处理器的方法并运行。
当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同一时候。意味着 Java 程序的终止。
对于运行时异常、错误或受检查的异常。Java技术所要求的异常处理方式有所不同:
由于运行时异常是不受检查的。Java规定:运行时异常将由Java运行时系统自己主动抛出,同意应用程序忽略运行时异常;
对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java同意该方法不做不论什么抛出声明。由于。大多数Error是不可恢复的,也属于合理的应用程序不该捕捉的异常。
对于全部受检查的异常,Java规定:异常必须被捕捉,或者进行异常说明。也就是说,当一个方法选择不捕捉可查异常时。它必须声明将抛出异常。
不论什么Java代码都能够抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。不管是谁。都能够通过Java的 throw 语句抛出异常。
整体来说,Java规定:对于可查异常必须捕捉、或者声明抛出。同意忽略不可查的 RuntimeException 和 Error。
2、 异常说明
对于受检查异常而言,Java提供了相应的语法。使你能告知client程序猿某个方法可能会抛出的异常类型,然后client程序猿就能够进行相应的处理。这就是异常说明。它属于方法声明的一部分。紧跟在形式參数列表之后,如以下的代码所看到的:
void f() throws TooBig, TooSmall, DivZero { ... }
表示 方法f 可能会抛出TooBig, TooSmall, DivZero三种异常,而
void g() { ... ... }
表示 方法g 不会抛出不论什么异常。
代码必须与异常说明保持一致。
若方法中的代码产生了受检查异常却没有进行处理,编译器就会发现这个问题并提醒你:要么处理这个异常,要么在异常说明中表明此方法将产生异常。只是,我们能够声明方法将抛出异常。但实际上并不抛出。
3、捕获异常
监控区域:它是一段可能产生异常的代码,而且后面跟着处理这些异常的代码,由 try…catch… 子句 实现。
(1) try 子句
假设方法内部抛出了异常,这种方法将在抛出异常的过程中结束。若不希望方法就此结束。能够在方法内设置一个特殊的块来捕获异常。当中,在这个块里,尝试各种方法调用的部分称为 try 块:try { // Code that might generate exceptions }
(2) catch 子句 – 异常处理程序
抛出的异常必须得到处理。而且针对每一个要捕获的异常。都必须准备相应的异常处理程序。异常处理程序必须紧跟在 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 }
异常处理程序可能用不到标识符(id1,id2,…)。由于异常的类型本身就已经给出了足够的信息来处理异常,但标识符不可省。
当异常被抛出时,异常处理机制将负责搜寻參数与异常类型相匹配的第一个处理程序。
然后进入相应的catch自居运行。此时觉得异常得到处理。一旦catch子句结束,则处理程序的查找结束(与 switch…case…不同)。
特别须要注意的是:异常匹配原则:抛出异常时,异常处理系统会依照代码的书写顺序找出近期匹配(派生类的对象能够匹配其基类的处理程序)的处理程序。
一旦找到,它就觉得异常将得到处理,然后停止查找;
不可屏蔽派生类异常:捕获基类异常的catch子句必须放在捕获其派生类异常的catch子句之后,否则编译不会通过。
catch子句 必须与 try子句 连用。
(3) finally 子句
The finally Block Description
The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return,continue, or break. Putting cleanup code in a finally block is always a good practice, even when no exceptions are anticipated.
Note: If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the application as a whole continues.
- finally 子句 总会被运行(前提:相应的 try子句 运行) 以下代码就没有运行 finally 子句:
public class Test { public static void main(String[] args) { System.out.println("return value of test(): " + test()); } public static int test() { int i = 1; System.out.println("the previous statement of try block"); i = i / 0; try { System.out.println("try block"); return i; }finally { System.out.println("finally block"); } } }/* Output: the previous statement of try block Exception in thread "main" java.lang.ArithmeticException: / by zero at com.bj.charlie.Test.test(Test.java:15) at com.bj.charlie.Test.main(Test.java:6) *///:~
当代码抛出一个异常时,就会终止方法中剩余代码的运行,同一时候退出该方法的运行。
假设该方法获得了一些本地资源。而且这些资源(eg:已经打开的文件或者网络连接等)在退出方法之前必须被回收,那么就会产生资源回收问题。这时,就会用到finally子句。示比例如以下:
InputStream in = new FileInputStream(...);try{ ...}catch (IOException e){ ...}finally{ in.close();}
finally 子句与控制转移语句的运行顺序
A finally clause can also be used to clean up for break, continue and return, which is one reason you will sometimes see a try clause with no catch clauses. When any control transfer statement is executed, all relevant finally clauses are executed. There is no way to leave a try block without executing its finally clause.
先看四段代码:
// 代码片段1 public class Test { public static void main(String[] args) { try { System.out.println("try block"); return ; } finally { System.out.println("finally block"); } } }/* Output: try block finally block *///:~
// 代码片段2public class Test { public static void main(String[] args) { System.out.println("reture value of test() : " + test()); } public static int test(){ int i = 1; try { System.out.println("try block"); i = 1 / 0; return 1; }catch (Exception e){ System.out.println("exception block"); return 2; }finally { System.out.println("finally block"); } } }/* Output: try block exception block finally block reture value of test() : 2 *///:~
// 代码片段3public class ExceptionSilencer { public static void main(String[] args) { try { throw new RuntimeException(); } finally { // Using ‘return’ inside the finally block // will silence any thrown exception. return; } } } ///:~
// 代码片段4class 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 lm = new LostMessage(); try { lm.f(); } finally { lm.dispose(); } } catch(Exception e) { System.out.println(e); } } } /* Output: A trivial exception *///:~
从上面的四个代码片段,我们能够看出,finally子句 是在 try 或者 catch 中的 return 语句之前运行的。更加一般的说法是,finally子句 应该是在控制转移语句之前运行。控制转移语句除了 return 外,还有 break 和 continue。另外,throw 语句也属于控制转移语句。尽管 return、throw、break 和 continue 都是控制转移语句,可是它们之间是有差别的。当中 return 和 throw 把程序控制权转交给它们的调用者(invoker),而 break 和 continue 的控制权是在当前方法内转移。
以下。再看两个代码片段:
// 代码片段5public class Test { public static void main(String[] args) { System.out.println("return value of getValue(): " + getValue()); } public static int getValue() { try { return 0; } finally { return 1; } } }/* Output: return value of getValue(): 1 *///:~
// 代码片段6public class Test { public static void main(String[] args) { System.out.println("return value of getValue(): " + getValue()); } public static int getValue() { int i = 1; try { return i; } finally { i++; } } }/* Output: return value of getValue(): 1 *///:~
利用我们上面分析得出的结论:finally子句 是在 try子句 或者 catch子句 中的 return 语句之前运行的。 由此。能够轻松的理解代码片段 5 的运行结果是 1。由于 finally 中的 return 1。语句要在 try 中的 return 0;语句之前运行,那么 finally 中的 return 1;语句运行后。把程序的控制权转交给了它的调用者 main()函数,而且返回值为 1。
那为什么代码片段 6 的返回值不是 2,而是 1 呢? 依照代码片段 5 的分析逻辑,finally 中的 i++;语句应该在 try 中的 return i;之前运行啊? i 的初始值为 1,那么运行 i++;之后为 2,再运行 return i。那不就应该是 2 吗?怎么变成 1 了呢? 关于 Java 虚拟机是怎样编译 finally 子句的问题,有兴趣的读者能够參考《 The JavaTM Virtual Machine Specification, Second Edition 》中 7.13 节 Compiling finally。那里具体介绍了 Java 虚拟机是怎样编译 finally 子句。
实际上。Java 虚拟机会把 finally 子句作为 subroutine 直接插入到 try 子句或者 catch 子句的控制转移语句之前。可是,还有另外一个不可忽视的因素,那就是在运行 subroutine(也就是 finally 子句)之前。try 或者 catch 子句会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 运行完成之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。
请注意,前文中我们以前提到过 return、throw 和 break、continue 的差别,对于这条规则(保留返回值)。仅仅适用于 return 和 throw 语句,不适用于 break 和 continue 语句,由于它们根本就没有返回值。
以下再看最后三个代码片段:
// 代码片段7public class Test { public static void main(String[] args) { System.out.println("return value of getValue(): " + getValue()); } @SuppressWarnings("finally") public static int getValue() { int i = 1; try { i = 4; } finally { i++; return i; } } }/* Output: return value of getValue(): 5 *///:~
// 代码片段8public class Test { public static void main(String[] args) { System.out.println("return value of getValue(): " + getValue()); } public static int getValue() { int i = 1; try { i = 4; } finally { i++; } return i; } }/* Output: return value of getValue(): 5 *///:~
// 代码片段9public class Test { public static void main(String[] args) { System.out.println(test()); } public static String test() { try { System.out.println("try block"); return test1(); } finally { System.out.println("finally block"); } } public static String test1() { System.out.println("return statement"); return "after return"; } }/* Output: try block return statement finally block after return *///:~
请注意,最后个案例的唯一一个须要注意的地方就是,return test1(); 这条语句等同于 :
String tmp = test1(); return tmp;
因而会产生上述输出。
特别须要注意的是,在以下4种特殊情况下,finally子句不会被(全然)运行:
1)在 finally 语句块中发生了异常; 2)在前面的代码中用了 System.exit()【JVM虚拟机停止】退出程序; 3)程序所在的线程死亡; 4)关闭 CPU;四. 异常的限制
当覆盖方法时。仅仅能抛出在基类方法的异常说明里列出的那些异常。这意味着,当基类使用的代码应用到其派生类对象时,一样能够工作。
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"); } } } ///:~
异常限制对构造器不起作用
子类构造器不必理会基类构造器所抛出的异常。然而。由于基类构造器必须以这样或那样的方式被调用(这里默认构造器将自己主动被调用),派生类构造器的异常说明必须包括基类构造器的异常说明。
派生类构造器不能捕获基类构造器抛出的异常
由于 super() 必须位于子类构造器的第一行,而若要捕获父类构造器的异常的话,则第一行必须是 try 子句。这样会导致编译不会通过。
派生类所重写的方法抛出的异常列表不能大于父类该方法的异常列表,即前者必须是后者的子集
通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。须要指出的是,派生类方法能够不抛出不论什么异常,即使基类中相应方法具有异常说明。也就是说,一个出如今基类方法的异常说明中的异常,不一定会出如今派生类方法的异常说明里。
异常说明不是方法签名的一部分
尽管在继承过程中,编译器会对异常说明做强制要求。但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字及其參数列表组成。因此,不能基于异常说明来重载方法。
五. 自己定义异常
使用Java内置的异常类能够描写叙述在编程时出现的大部分异常情况。
除此之外,用户还能够自己定义异常。用户自己定义异常类。仅仅需继承Exception类就可以。
在程序中使用自己定义异常类,大体可分为以下几个步骤:
(1)创建自己定义异常类;
(2)在方法中通过throw关键字抛出异常对象; (3)假设在当前抛出异常的方法中处理异常。能够使用try-catch语句捕获并处理。否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作; (4)在出现异常方法的调用者中捕获并处理异常。六. 异常栈与异常链
1、栈轨迹
printStackTrace() 方法能够打印Throwable和Throwable的调用栈轨迹。调用栈显示了由异常抛出点向外扩散的所经过的全部方法。即方法调用序列(main方法 一般是方法调用序列中的最后一个)。
2、又一次抛出异常
catch(Exception e) { System.out.println("An exception was thrown"); throw e; }
既然已经得到了对当前异常对象的引用,那么我们就能够像上面一样将其又一次抛出。又一次抛出的异常会把异常抛给上一级环境中的异常处理程序。同一个try子句的兴许catch子句将被忽略。
此外。假设仅仅是把当前异常对象又一次抛出,那么printStackTrace() 方法显示的仍是原来异常抛出点的调用栈信息,而并不是又一次抛出点的信息。要想更新这个信息,能够调用fillInStackTrace() 方法,这将返回一个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); } try { h(); } catch(Exception e) { System.out.println("main: printStackTrace()"); e.printStackTrace(System.out); } } } /* Output: originating the exception in f() Inside g(),e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:7) at Rethrowing.g(Rethrowing.java:11) at Rethrowing.main(Rethrowing.java:29) main: printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:7) at Rethrowing.g(Rethrowing.java:11) at Rethrowing.main(Rethrowing.java:29) originating the exception in f() Inside h(),e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:7) at Rethrowing.h(Rethrowing.java:20) at Rethrowing.main(Rethrowing.java:35) main: printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.h(Rethrowing.java:24) at Rethrowing.main(Rethrowing.java:35) *///:~
3、异常链
异常链:在捕获一个异常后抛出还有一个异常。而且希望把原始异常的信息保存下来。
这能够使用带有cause參数的构造器(在Throwable的子类中,仅仅有Error,Exception和RuntimeException三个类提供了带有cause的构造器)或者使用initcause()方法把原始异常传递给新的异常。使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。比如:class DynamicFieldsException extends Exception {}...DynamicFieldsException dfe = new DynamicFieldsException(); dfe.initCause(new NullPointerException()); throw dfe;...//捕获该异常并打印其调用站轨迹为:/**DynamicFieldsException at DynamicFields.setField(DynamicFields.java:64) at DynamicFields.main(DynamicFields.java:94) Caused by: java.lang.NullPointerException at DynamicFields.setField(DynamicFields.java:66) ... 1 more*/
以 RuntimeException 及其子类NullPointerException为例,其源代码分别为:
RuntimeException 源代码包括四个构造器。有两个可接受cause:public class RuntimeException extends Exception { static final long serialVersionUID = -7034897190745766939L; /** Constructs a new runtime exception withnull
as its * detail message. The cause is not initialized, and may subsequently be * initialized by a call to {@link #initCause}. */ public RuntimeException() { super(); } /** Constructs a new runtime exception with the specified detail message. * The cause is not initialized, and may subsequently be initialized by a * call to {@link #initCause}. * * @param message the detail message. The detail message is saved for * later retrieval by the {@link #getMessage()} method. */ public RuntimeException(String message) { super(message); } /** * Constructs a new runtime exception with the specified detail message and * cause.Note that the detail message associated with *
cause
is not automatically incorporated in * this runtime exception's detail message. * * @param message the detail message (which is saved for later retrieval * by the {@link #getMessage()} method). * @param cause the cause (which is saved for later retrieval by the * {@link #getCause()} method). (A null value is * permitted, and indicates that the cause is nonexistent or * unknown.) * @since 1.4 */ public RuntimeException(String message, Throwable cause) { super(message, cause); } /** Constructs a new runtime exception with the specified cause and a * detail message of (cause==null ? null : cause.toString()) * (which typically contains the class and detail message of * cause). This constructor is useful for runtime exceptions * that are little more than wrappers for other throwables. * * @param cause the cause (which is saved for later retrieval by the * {@link #getCause()} method). (A null value is * permitted, and indicates that the cause is nonexistent or * unknown.) * @since 1.4 */ public RuntimeException(Throwable cause) { super(cause); }}
NullPointerException 源代码仅包括两个构造器。均不可接受cause:
public class NullPointerException extends RuntimeException { /** * Constructs aNullPointerException
with no detail message. */ public NullPointerException() { super(); } /** * Constructs aNullPointerException
with the specified * detail message. * * @param s the detail message. */ public NullPointerException(String s) { super(s); }}
注意:
全部的标准异常类都有两个构造器:一个是默认构造器。还有一个是接受字符串作为异常说明信息的构造器。
引用:
《Java编程思想(第四版)》