JavaSE-Adventure(Ⅳ) Exceptions 异常机制

JavaSE-Adventure(Ⅳ) Exceptions 异常机制

发现错误的理想时机是在编译阶段。也就是在你试图运行程序之前。

然而,编译期间编译器并不能找出全部的错误,余下的错误仅仅有在运行期才干发现和解决,这类错误就是 Throwable

在 Java 中,如果某个方法不能够采用正常的途径完成它的任务,就可以通过另外一个途径退出方法。这种情况下,方法不返回任何值而是抛出一个封装了异常信息的对象

终止模型

终止模型,也是Java 所支持的异常模型。

在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,表明错误无法返回,无法回来继续执行。

相对于终止模型,还有一种异常处理模型为 恢复模型。它使异常被处理之后能够继续运行程序。尽管该模型非常吸引人,但不是非常实用,其主要原因是它所导致的耦合:恢复性处理程序须要了解异常的抛出地点。这势必要包括依赖于抛出位置的非通用代码,从而大大添加了代码编写和维护的难度。

异常分类

在这里插入图片描述

  • Throwable
    Java 异常体系设计顶级父类是 Throwable

  • Error
    Errorr 往往是很严重的错误,与代码运行时的JVM、资源等有关,是程序无法处理的异常,我们没办法通过程序进行捕获。如:

    • StackOverflowError: 栈内存溢出错误(递归层次太深,或递归没有结束)
    • OutOfMemorryError: 堆内存溢出(堆创建了太多对象)
    • ThreadDeath: 线程死亡
  • Exception
    Exception是程序本身可以处理的异常,可以通过catch捕捉。它由两个分支组成: 运行时异常(派生于 RuntimeException 的异常) 和 其它异常。
    RuntimeException(也叫LogicException), 发生RuntimeException 一定是代码逻辑出现了问题,比较典型的:

    • NullPointerException:程序不够严谨没有进行非空判断
    • IndexOutOfBoundceException:对数组的边界没有严格控制,访问数组初始化长度以外的索引位置
    • ClassCastException:将一个不是该类的实例转换成这个类就会抛出这个异常
    • IOException:进行IO操作时会发生的异常:FileNotFoundExcepion, EOFException

受检查的异常和非受检查的异常 (checked/unchecked exceptions)

撇开Error 不谈,除了RuntimeException (unchecked)以外的异常(如:Throwable,Exception,IOException 或是继承自他们的异常)都被称为:CheckedException,也就是编译器会要求我们对这类异常进行捕获 (try-catch) 或抛出,否则会编译报错。

异常处理

在 Java 应用程序中。异常处理机制为:抛出异常 与 捕捉异常。

  1. 抛出异常
    当一个方法出现错误引发异常时。方法创建异常对象(声明方法将抛出异常) 并交付运行时系统,异常对象中包括了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处理异常的代码并运行。

  2. 捕获异常
    在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时。即为合适 的异常处理器。运行时系统从发生异常的方法開始,依次回查调用栈中的方法。直至找到含有合适异常处理器的方法并运行。

当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同一时候。意味着 Java 程序的终止。

捕获异常

监控区域 (guarded block / catch block)

它是一段可能产生异常的代码,而且后面跟着处理这些异常的代码,由 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 
} 

当异常被抛出时,异常处理机制将负责搜寻參数与异常类型相匹配的第一个处理程序(catch block)。

finally block

不管是否有异常被捕获,finally 子句中的代码都会被执行。
Oracle documentation:

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: The finally block may not execute if the JVM exits while the try or catch code is being executed.

官方文档描述finally 总会被运行(前提:相应的 try子句 运行),即使是try代码块中包含 return, continue, break等语句。


If the try clause executes a return, the compiled code does the following:

  1. Saves the return value (if any) in a local variable.
  2. Executes a jsr to the code for the finally clause.
  3. Upon return from the finally clause, returns the value saved in the local variable.

当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略

这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

finally 引申出的问题

1. 什么情况下 finally 代码块不会执行?
  1. try语句没有被执行到,如在try语句之前就返回了,这样finally语句就不会执行,这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到。

  2. 在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。

2. finally 语句在return 语句执行之后,return 返回之前执行
public static int test1() {
    int b = 20;
    try {
        System.out.println("try block" ); 
        return b += 80;
    }
    catch (Exception e) {
        System.out.println("catch block");
    }
    finally {
        System.out.println("finally block"); 
        if (b > 25) {
            System.out.println("b>25, b = " + b);
        }
    }
    return b;
}
System.out.println(test1()); 
// ...output
try block
finally block
b>25, b = 100
100

上述例子中说明return语句已经执行了(完成了计算)再去执行finally语句,不过并没有直接返回,而是等finally语句执行完了再返回结果。



public static String test2() {
    try {
        System.out.println("try block");
        return test21();
    } finally {
        System.out.println("finally block");
    }
}

public static String test21() {
    System.out.println("return statement");

    return "after return";
}

System.out.println(test2());
// output..
try block
return statement
finally block
after return

上述例子同样说明try中的return语句先执行了(执行了test21()方法)但并没有立即返回,等到finally执行结束后再返回。

3. finally 中的return 会覆盖 try 中的return返回
public static int test3() {
    int b = 20;
    try {
        System.out.println("try block");
        return b += 80;
    } catch (Exception e) {
        System.out.println("catch block");
    } finally {
        System.out.println("finally block");
        if (b > 25) {
            System.out.println("b>25, b = " + b);
        }
        return 200;
    }
}
System.out.println(test3()); 
// ..output
try block
finally block
b>25, b = 100
200

这说明finally里的return直接返回了,就不管try中是否还有返回语句。

4. 如果finally 中没有return,但是修改了 try中的return 值,结果回返回什么?

这里可以把finally 代码块想象成一个方法,return 值是传递给finally 方法中的参数。

因为Java 是值传递的,那么这里分为两种情况,返回值是基本类型和返回值是引用类型。

public static int test4() {
    int b = 20;
    try {
        System.out.println("try block");
        return b += 80;
    } catch (Exception e) {
        System.out.println("catch block");
    } finally {
        System.out.println("finally block");
        if (b > 25) {
            System.out.println("b>25, b = " + b);
        }
        b = 150;
    }
    return 0;
}
System.out.println(test4());
// ...output
try block
finally block
b>25, b = 100

100这里可以看出 返回值从try 传递到finally 中的只是值的拷贝,因此在finally 中修改返回值并不影响到try 中真正的返回值。


public static Student test5() {
    Student student = new Student();
    try {
        System.out.println("try block");
        student.setStuId(2L);
        return student;
    } catch (Exception e) {
        System.out.println("catch block");
    } finally {
        System.out.println("finally block");
        student.setStuId(3L);
    }
    student.setStuId(0L);
    return student;
}

System.out.println(test5().getStuId()); // 3L

返回值从try 传递到finally, 因为传递的是对象的引用, 在finally中对对象的域进行修改, 同样会影响实际的值。

5. try 里的return 在发生异常语句的后面时不会执行
public static int test6() {
    int b = 20;
    try {
        System.out.println("try block");
        b = b / 0;
        return b += 80;
    } catch (Exception e) {
        b += 15;
        System.out.println("catch block");
    } finally {
        System.out.println("finally block");
        if (b > 25) {
            System.out.println("b>25, b = " + b);
        }
        b += 50;
    }
    return b;
}
System.out.println(test6());
// output..
try block
catch block
finally block
b>25, b = 35
85
6. 当异常发生,catch 中的return 执行情况与try 里的执行情况一样
  1. return 语句执行 ->
  2. return 返回值传递(primitive/reference) ->
  3. finally --> (finally 的return 返回值(如果有)) ->
  4. return 返回值

更详细的关于 Java 虚拟机是怎样编译 finally 子句的问题,有兴趣的读者能够參考《 The JavaTM Virtual Machine Specification, Second Edition 》中 7.13 节 Compiling finally。

异常的限制

  • 当覆盖方法时,仅仅能抛出在基类方法的异常说明里列出的那些异常。这意味着,当基类使用的代码应用到其派生类对象时,一样能够工作。
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 子句。这样会导致编译不会通过。

  • 派生类所重写的方法抛出的异常列表不能大于父类该方法的异常列表,即前者必须是后者的子集
    通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。须要指出的是,派生类方法能够不抛出不论什么异常,即使基类中相应方法具有异常说明。也就是说,一个出如今基类方法的异常说明中的异常,不一定会出如今派生类方法的异常说明里。

  • 异常说明不是方法签名的一部分
    尽管在继承过程中,编译器会对异常说明做强制要求。但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字及其參数列表组成。因此,不能基于异常说明来重载方法。

异常栈与异常链

栈轨迹

printStackTrace() 方法能够打印Throwable和Throwable的调用栈轨迹。调用栈显示了由异常抛出点向外扩散的所经过的全部方法。即方法调用序列(main方法 一般是方法调用序列中的最后一个)。

又一次抛出异常

既然已经得到了对当前异常对象的引用,那么我们就能够像上面一样将其又一次抛出。

假设仅仅是把当前异常对象又一次抛出,那么printStackTrace() 方法显示的仍是原来异常抛出点的调用栈信息,而并不是又一次抛出点的信息。要想更新这个信息,能够调用fillInStackTrace() 方法,这将返回一个Throwable对象。它是通过把当前调用栈信息填入原来那个异常对象而建立的。

try { 
    f(); 
} catch(Exception e) { 
    System.out.println("Inside h(),e.printStackTrace()"); 
    e.printStackTrace(System.out); 
    throw (Exception)e.fillInStackTrace(); 
} 

异常链

异常链:在捕获一个异常后抛出还有一个异常。而且希望把原始异常的信息保存下来。
 
这能够使用带有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
*/

Java7 try-with-resources

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。

随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

通过使用分号分隔,可以在try-with-resources块中声明多个资源

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
                BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
} catch (IOException e) {
    e.printStackTrace();
}

Java finally语句到底是在return之前还是之后执行?:https://www.cnblogs.com/lanxuezaipiao/p/3440471.html
Java 异常模型综述:http://t.zoukankan.com/claireyuancy-p-7350261.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值