|
|
第10章 通过异常处理错误
一. 基本异常 1. 抛出异常的原理 1) 像产生一个Java对象那样在heap上以new产生一个异常对象。 2) 停止目前的执行路线,将上述那个异常对象的reference自目前的context丢出。 3) 异常处理机制接手工作,寻找得以继续执行的适当地点。 2. 产生一个异常对象 异常类有两个构造函数:一个default构造函数;一个带String型参数的构造函数,参数的信息可以通过异常类中的各种方法取出。 3. 异常类的结构  1) Error是一些编译期错误或系统错误,一般无需在程序中捕捉到Error异常。 2) Exception是我们能捕捉到的异常,其中Exception异常又分为RuntimeException和non-RuntimeException两大类异常。
二. 异常的捕捉和处理 1. 异常的捕捉 1.1 通过try…catch就可捕捉异常
运行结果为:catch SQLException in main 只有参数类型与异常类型相同或相近的catch会被执行。 1.2 捕捉所有异常 如果想捕捉所有异常,只要捕捉Exception异常就行,如上面代码的(1)处 2. 异常规格(exception specification) 1) 在函数定义时可以声明异常规格。如果一个函数在异常规格中声明了non-RuntimeException异常,那么当调用这个函数时,就一定要捕捉异常规格中的non-RuntimeException异常。
- import java.lang.RuntimeException;
- import java.lang.NullPointerException;
- import java.sql.SQLException;
- class TestException{
- //(1)异常规格中声明将抛出RuntimeException异常
- public void testRuntime() throws RuntimeException {}
- //(2)异常规格中声明将抛出NullPointerException异常
- public void testNullPointer() throws NullPointerException {}
- //(3)异常规格中声明将抛出non-RuntimeException异常
- public void testNonRuntime() throws SQLException {}
- }
- public class Test{
- public static void main(String[] args){
- TestException te = new TestException();
- te.testRuntime(); //(4)
- te.testNullPointer(); //(5)
- //te.testNonRuntime(); (6)
- try{
- te.testNonRuntime();
- }
- catch(SQLException ex){}
- }
- }
在上述代码中,(1)处在异常规格中声明将抛出RuntimeException;(2)在异常规格中声明将抛出NullPointerException,而NullPointerException是RuntimeException的子类,所以在调用这两个函数时,可不捕捉异常,如(4)(5)处的代码一样直接调用。但(3)处在异常规格中声明将抛出SQLException,而SQLException不是RuntimeException的子类,所以必须捕捉SQLException异常。 2) 如果要在一个函数中抛出non-RuntimeException异常,则必须要在异常规格中声明该异常。
- import java.sql.SQLException;
- import java.io.IOException;
- class Test1{
- public void f() throws SQLException{ //(2)
- throw new IOException("IOException"); //(1)
- }
- }
- public class ExplicitStatic{
- public static void main(String[] args){
- Test1 te = new Test1();
- try{
- te.f();
- }
- catch(Exception ex){
- System.out.println("catch Exception in main");
- }
- }
- }
在(1)处抛出了一个没有在异常规格中被声明的non-RuntimeException异常,在编译时会出错。 3. 取得异常中的信息的几个函数 1) String getMessage()、getLocalizedMessage 、toString 取得异常对象中的信息
- import java.lang.RuntimeException;
- import java.lang.NullPointerException;
- Import java.sql.SQLException;
- import java.io.IOException;
- class TestException{
- public void tSql() throws SQLException {
- System.out.println("Originating the exception in tSql()");
- throw new SQLException("throw in tSql");
- }
- }
- public class Test{
- public static void main(String[] args){
- TestException te = new TestException();
- try{
- te.tSql();
- }
- catch(SQLException ex){
- System.out.println("catch SQLException in main");
- System.out.println("ex.getMessage():" + ex.getMessage());
- System.out.println("ex.getLocalizedMessage():" +
- ex.getLocalizedMessage());
- System.out.println("ex.toString():" + ex.toString());
- }
- catch(Exception ex){
- System.out.println("catch Exception in main");
- }
- }
- }
运行结果: Originating the exception in tSql() catch SQLException in main ex.getMessage():throw in tSql ex.getLocalizedMessage():throw in tSql ex.toString():java.sql.SQLException: throw in tSql 2) void printStackTrace()、Throwable fillStackTrace() printStackTrace打印出Throwable和其call stack trace。 FillStackTrace则在调用点设立新的stack trace信息
- import java.sql.SQLException;
- class TestException{
- public static void tSql() throws SQLException {
- System.out.println("Originating the exception in tSql()");
- throw new SQLException("throw in tSql");
- }
- public void f() throws SQLException{
- try{
- tSql();
- }
- catch(SQLException ex){
- System.out.println("In f(), e.printStackTrace()");
- ex.printStackTrace();
- throw ex; //(1)
- //throw (SQLException)ex.fillInStackTrace(); (2)
- }
- }
- }
- public class Test{
- public static void main(String[] args){
- TestException te = new TestException();
- try{
- te.f();
- }
- catch(SQLException ex){
- System.out.println("catch in main, e.printStackTrace()");
- ex.printStackTrace();
- }
- catch(Exception ex){
- System.out.println("catch Exception in main");
- }
- }
- }
结果为: Originating the exception in tSql() In f(), e.printStackTrace() catch in main, e.printStackTrace() java.sql.SQLException: throw in tSql void TestException.tSql() Test.java:5 void TestException.f() Test.java:9 void Test.main(java.lang.String[]) Test.java:22 java.sql.SQLException: throw in tSql void TestException.tSql() Test.java:5 void TestException.f() Test.java:9 void Test.main(java.lang.String[]) Test.java:22 如果把(1)处代码注释掉,并去年(2)处代码的注释,结果将变成: Originating the exception in tSql() In f(), e.printStackTrace() catch in main, e.printStackTrace() java.sql.SQLException: throw in tSql void TestException.tSql() //(3) Test.java:6 void TestException.f() Test.java:10 void Test.main(java.lang.String[]) Test.java:24 java.sql.SQLException: throw in tSql void TestException.f() //(4) Test.java:16 void Test.main(java.lang.String[]) Test.java:24 由于在代码(2)处设立新的stack trace信息,所以异常会被认为是在f()中发出的,所以在main()中得到的异常原始抛出点为f()(见(3)),而在f()中为tSql()(见(6))。 3) 如果重新抛出一个不同类型的异常,也能产生fillStackTrace()函数的效果。如果把上面代码的f()函数修改成下面的样子:
- public void f() throws SQLException,IOException{
- try{
- tSql();
- }
- catch(SQLException ex){
- System.out.println("In f(), e.printStackTrace()");
- ex.printStackTrace();
- throw new IOException(); //(1)
- }
- }
则结果为: Originating the exception in tSql() In f(), e.printStackTrace() catch Exception in main java.sql.SQLException: throw in tSql void TestException.tSql() Test.java:6 void TestException.f() Test.java:10 void Test.main(java.lang.String[]) Test.java:25 java.io.IOException void TestException.f() Test.java:17 void Test.main(java.lang.String[]) Test.java:25 由于在(1)处抛出了一个新的类型的异常,那么在main()中捕捉到的是新的异常,所以在main()中捕捉到的异常的原始抛出点为f()。 4. RuntimeException异常 RuntimeException及其子类所代表的异常我们在程序中不用进行捕捉,如果发生此类异常,Java会自动抛出相应的异常对象,如:
- class TestException{
- public static void g(int x) {
- System.out.println("10/" + x + " = " + 10/x);
- }
- }
- public class Test{
- public static void main(String[] args){
- TestException.g(2);
- TestException.g(0); //(1)
- }
- }
上面代码在编译时不会发生错误,只有在运行时(1)处会发生错误。虽然除法可能会存在错误,但我们不用进行捕捉,当发生错误时,Java会自动抛出相应异常。
三. 以finally进行清理 1. 如果某段代码不管是否发生异常都要执行,那可把它改入finally块中。
- import java.sql.SQLException;
- class TestException{
- public static void tSql() throws SQLException {
- System.out.println("Originating the exception in tSql()");
- throw new SQLException("throw in tSql");
- }
- public void f() throws SQLException{
- try{
- tSql();
- }
- catch(SQLException ex){
- System.out.println("catch SQLException in f()");
- throw ex; //(1)
- }
- finally{
- System.out.println("finally in f()");
- }
- }
- }
- public class Test{
- public static void main(String[] args){
- TestException te = new TestException();
- try{
- te.f();
- }
- catch(SQLException ex){
- System.out.println("catch te.f() SQLException in main");
- }
- catch(Exception ex){
- System.out.println("catch te.f() Exception in main");
- }
- }
- }
运行结果为: Originating the exception in tSql() catch SQLException in f() finally in f() catch te.f() SQLException in main 虽然在代码(1)处重新抛出异常,但finally块中的代码仍然会被执行。 2. finally造成的异常遗失 如果在finally中执行的代码又产生异常,那么在上一层调用中所捕捉到的异常的起始抛出点会是finally所在的函数。
- import java.sql.SQLException;
- class TestException{
- public static void tSql1() throws SQLException {
- System.out.println("Originating the exception in tSql()");
- throw new SQLException("throw in tSql1");
- }
- public static void tSql2() throws SQLException {
- System.out.println("Originating the exception in tSql()");
- throw new SQLException("throw in tSql2");
- }
- public void f() throws SQLException{
- try{
- tSql1();
- }
- catch(SQLException ex){
- System.out.println("catch SQLException in f()");
- throw ex; //(2)
- }
- finally{
- System.out.println("finally in f()");
- //tSql2(); (1)
- }
- }
- }
- public class Test{
- public static void main(String[] args){
- TestException te = new TestException();
- try{
- te.f();
- }
- catch(SQLException ex){
- System.out.println("catch te.f() SQLException in main");
- System.out.println("getMessage:" + ex.getMessage());
- System.out.println("printStackTrace:");
- ex.printStackTrace();
- }
- }
- }
运行结果为: Originating the exception in tSql() catch SQLException in f() finally in f() catch te.f() SQLException in main getMessage:throw in tSql1 printStackTrace: java.sql.SQLException: throw in tSql1 void TestException.tSql1() Test.java:5 void TestException.f() Test.java:13 void Test.main(java.lang.String[]) Test.java:29 从结果可以看出,在main()中能正确打印出所捕捉到的异常的起始抛出点。但如果去掉代码(1)的注释,结果将变为: Originating the exception in tSql() catch SQLException in f() finally in f() Originating the exception in tSql() catch te.f() SQLException in main getMessage:throw in tSql2 printStackTrace: java.sql.SQLException: throw in tSql2 void TestException.tSql2() Test.java:9 void TestException.f() Test.java:21 void Test.main(java.lang.String[]) Test.java:29 从结果可以看出,在main()中捕捉到的异常是finally中产生的异常,代码(2)中抛出的异常丢失了。
四. 继承中异常 1. 关于构造函数中的异常 1.1 构造函数中的异常规则 某个derived class构造函数的“异常规格接口“可以比其所调用的父类的构造函数的异常规格接口宽,但决不能变窄。 1) derived class的构造函数必须在自己的异常规格中声明所有base class构造函数的异常规格中所声明的异常。 2) 在derived class的构造函数的异常规格中还可以声明新的异常,即声明在base class构造函数的异常规格中没有声明的异常。 1.2 原因 当在产生一个derived class的对象时,会在derived class的构造函数中调用base class的构造函数(初始化过程请见第6章),所以在derived class的构造函数中可能会抛出base class构造函数的异常规格中声明的异常,因此要在derived class的异常规格中声明base class构造函数的异常规格中声明的异常。 **:如果调用的函数的异常规格中声明了异常,那么在调用该函数的时候要捕捉它的异常规格中声明的异常。但在derived class构造函数中却无法捕捉其base class构造函数所掷出的异常。 2. 关于非构造函数的异常规则 2.1 某个函数的“异常规格接口“在继承和重载中可以变窄,但决不能变宽 要覆写base class的函数时,如果被覆写函数(base class中的函数)的异常规格中声明了异常,那么覆写函数(derived class中覆写了base class中的函数的那个函数)的异常规格中可以声明(1)与被覆写函数完全相同的异常;(2)被覆写函数异常规格中的部分异常或其子类异常;(3)不声明异常规格。 2.2 原因 这么做是为了满足“能处理被覆写函数的代码,不用做任何修改就能处理覆写函数的代码”的原则。 如果覆写函数的异常规格中声明了在被覆写函数的异常规格中不存在的异常,那么能处理被覆写函数的代码就不能处理覆写函数,因为没有捕捉覆写函数中不存在于被覆写函数中的异常声明。
- import java.sql.SQLException;
- class BaseClass{
- public void f(){}
- }
- class DerivedClass1 extends BaseClass{
- //public void f() throws SQLException {} (1)
- public void f() {} //(2)
- }
- public class Test{
- public static void f(BaseClass bc) { bc.f(); }
- /* (3)
- public static void f(BaseClass bc) {
- try{
- bc.f();
- }
- catch(SQLException ex){}
- }
- */
- public static void main(String[] args){
- BaseClass bc = new BaseClass();
- f(bc);
- DerivedClass1 dc = new DerivedClass1();
- f(bc);
- }
- }
如果允许“异常接口“变宽,我们看看上面代码会出现什么结果。首先,我们可以将代码(1)的注释去掉,并注释掉代码(2)。由于BaseClass class中的被覆写f()函数没有声明异常规格,而代码(1)中覆写f()函数声明了,那么Test class中的f(BaseClass bc)虽然能处理被覆写f()函数的调用,但不能处理覆写f()函数的调用,因为代码覆写f()函数声明了异常规格,而f(BaseClass bc)没有进行捕捉。那么为了处理覆写f()函数,我们还要编写代码(3)那样的处理函数。 2.3 产生对象的异常规则 在产生一个对象时,捕捉的是产生对象时所调用的构造函数中所声明的异常。 2.4 函数调用时的异常规则 1) 当把一个对象向上转型为它的base class时,并通过转型后的reference进行函数调用时,我们要捕捉的是其base class的异常声明。 2) 当用对象的原始类型来调用函数时,只需捕捉所调用的覆写函数的异常 2.5 继承中的异常规则的一个实例
- import java.lang.Exception;
- class BaseException extends Exception {}
- class Derived1Exception extends BaseException {}
- class Derived2Exception extends BaseException {}
- class Derived11Exception extends Derived1Exception {}
- class BaseClass{
- BaseClass() throws Derived1Exception {}
- BaseClass(int i) throws BaseException {}
- BaseClass(int i, int j) {}
- //在覆写f()时不能声明异常规格
- public void f() {}
- //在覆写g()时可以不声明异常或声明BaseException异常或声明
- //BaseException异常的子类
- public void g() throws BaseException {}
- //在覆写u()时可以不声明异常
- public void u() throws Derived1Exception, Derived2Exception {}
- }
- class DerivedClass1 extends BaseClass{
- //base class构造函数中声明了异常的处理方法
- //声明与base class构造函数中的异常完全相同的异常
- DerivedClass1(int i) throws Derived1Exception {}
- //声明base class构造函数中的异常的父类异常
- DerivedClass1() throws BaseException {}
- //声明base class构造函数中的异常和新的异常
- DerivedClass1(String s) throws Derived1Exception, Derived2Exception{}
- //声明base class构造函数中的异常父类异常和新的异常
- DerivedClass1(String s, int i) throws BaseException, Derived2Exception{}
- DerivedClass1(int i, int j) { super(i, j); }
- //注意下面这两句
- DerivedClass1(int i, String s) throws BaseException { super(i); }
- //!DerivedClass1(int i, String s) throws Derived1Exception { super(i);}
- public void f() {}
- //下面覆写g()的几种方式
- //不声明
- //public void g() {}
- //public void g() throws BaseException {} 声明完全相同
- //声明子类异常
- public void g() throws Derived1Exception {}
- //声明子类异常
- //public void g() throws Derived1Exception, Derived11Exception {}
- //下面覆写u()的几种方式
- //public void u() {} 不声明
- //public void u() throws Derived11Exception {} 声明部分异常的异常
- //public void u() throws Derived1Exception {} 声明部分异常
- //声明完全相同
- //public void u() throws Derived1Exception, Derived2Exception {}
- //声明子类异常
- //public void u() throws Derived1Exception, Derived11Exception {}
- }
- public class Test{
- public static void main(String[] args){
- //捕捉的是相应的构造函数的异常
- try{
- BaseClass bc1 = new DerivedClass1();
- }
- catch(BaseException be) {}
- try{
- BaseClass bc2 = new DerivedClass1("bc2");
- }
- catch(Derived1Exception be1) {}
- catch(Derived2Exception be2) {}
- //通过向上转型来调用函数
- BaseClass bc3 = new DerivedClass1(1, 1);
- /*捕捉的是父类中被覆写的函数的异常,而这里捕捉的是子类中
- * 被覆写的函数的异常,所以编译错误
- try{
- bc3.g();
- }
- catch(Derived1Exception be) {}
- */
- //捕捉了被覆写函数的异常,是正确的
- try{
- bc3.g();
- }
- catch(BaseException be) {}
- //用对象的原始类型来调用函数
- DerivedClass1 dc1 = new DerivedClass1(1, 1);
- //只需捕捉所调用的覆写函数的异常
- try{
- dc1.g();
- }
- catch(Derived1Exception be) {}
- }
- }
|
|