Exceptions
Exceptions 允许您顺利处理程序运行时发生的意外情况. 要演示 Java 虚拟机处理异常的方式, 请考虑一个名为 NitPickyMath 的类. 它提供了对整数执行加法, 减法, 乘法, 除法和余数的方法. NitPickyMath 在溢出, 下溢和被零除的条件下抛出已检查的异常. Java 虚拟机将在整数除零上抛出一个 ArithmeticException, 但不会在溢出和下溢上抛出任何异常. 方法抛出的异常定义如下:
- class OverflowException extends Exception {
- }
- class UnderflowException extends Exception {
- }
- class DivideByZeroException extends Exception {
- }
捕获和抛出异常的简单方法是 remainder 类的方法 NitPickyMath:
- static int remainder(int dividend, int divisor)
- throws DivideByZeroException {
- try {
- return dividend % divisor;
- }
- catch (ArithmeticException e) {
- throw new DivideByZeroException();
- }
- }
该 remainder 方法仅在传递两个 int 参数时执行余数运算. 如果余数运算的除数为零, 则余数运算抛出一个 ArithmeticException. 这个方法捕获了这个 ArithmeticException 并抛出一个 DivideByZeroException.
DivideByZeroException 和 ArithmeticException 之间的差别是 DivideByZeroException 是一个 检查 异常, 并且 ArithmeticException 是 未经检查 . 因为 ArithmeticException 是非受检异常, 所以方法不需要在 throws 子句中声明此异常, 即使它可能会抛出它. 任何属于 Error 或者 RuntimeException 子类的异常都是非受检异常.(ArithmeticException 是 RuntimeException 的子类.)通过捕获 ArithmeticException 然后抛出 DivideByZeroException, 该 remainder 方法强制其客户端处理除零异常的可能性, 通过捕获它或在自己的 throws 子句中声明 DivideByZeroException. 这是因为已检查的异常, 例如 DivideByZeroException, 抛出方法必须由方法捕获或在方法的 throws 子句中声明. 未经检查的异常(例如 ArithmeticException, 不需要在 throws 子句中捕获或声明).
javac 为该 remainder 方法生成以下字节码序列:
- The main bytecode sequence for remainder:
- 0 iload_0 // Push local variable 0 (arg passed as divisor)
- 1 iload_1 // Push local variable 1 (arg passed as dividend)
- 2 irem // Pop divisor, pop dividend, push remainder
- 3 ireturn // Return int on top of stack (the remainder)
- The bytecode sequence for the catch (ArithmeticException) clause:
- 4 pop // Pop the reference to the ArithmeticException
- // because it isn't used by this catch clause.
- 5 new #5 <Class DivideByZeroException>
- // Create and push reference to new object of class
- // DivideByZeroException.
- DivideByZeroException
- 8 dup // Duplicate the reference to the new
- // object on the top of the stack because it
- // must be both initialized
- // and thrown. The initialization will consume
- // the copy of the reference created by the dup.
- 9 invokenonvirtual #9 <Method DivideByZeroException.<init>()V>
- // Call the constructor for the DivideByZeroException
- // to initialize it. This instruction
- // will pop the top reference to the object.
- 12 athrow // Pop the reference to a Throwable object, in this
- // case the DivideByZeroException,
- // and throw the exception.
该 remainder 方法的字节码序列有两个独立的部分. 第一部分是该方法的正常执行路径. 这部分从 pc 偏移 0 到 3. 第二部分是 catch 子句, 它从 pc 偏移 4 到 12.
主字节码序列中的 irem 指令可能会抛出一个 ArithmeticException. 如果发生这种情况, Java 虚拟机知道通过查找表中的异常来跳转到实现 catch 子句的字节码序列. 捕获异常的每个方法都与一个异常表相关联, 该异常表在类文件中与方法的字节码序列一起传递. 每个 try 块捕获的每个异常在异常表中都有一个条目. 每个条目都有四条信息: 起点和终点, 要跳转到的字节码序列中的 pc 偏移量, 以及正被捕获的异常类的常量池索引. remainder 类的 NitPickyMath 方法的异常表如下所示:
- Exception table:
- from to target type
- 0 4 4 <Class java.lang.ArithmeticException>
上面的异常表指示从 pc 偏移 0 到 3(包括 0), 表示 ArithmeticException 将被捕获的范围. 在标签 "to" 下面的表中列出的是 try 块的端点值, 它总是比捕获异常的最后一个 pc 偏移量多一. 在这种情况下, 端点值列为 4, 捕获到异常的最后一个 pc 偏移量为 3. 此范围 (包括 0 到 3) 对应于在 remainder 的 try 块内实现代码的字节码序列. 如果 ArithmeticException 在 pc 偏移量为 0 和 3 之间 (包括 0 和 3) 之间抛出, 则表中列出的 "to" 就是跳转到的 pc 偏移量.
如果在执行方法期间抛出异常, Java 虚拟机将在异常表中搜索匹配的条目. 如果当前程序计数器在条目指定的范围内, 并且抛出的异常类是由条目指定的异常类(或者是指定异常类的子类), 则异常表条目匹配. Java 虚拟机按照条目在表中的显示顺序搜索异常表. 找到第一个匹配项后, Java 虚拟机会将程序计数器设置为新的 pc 偏移位置并继续执行. 如果未找到匹配项, Java 虚拟机将弹出当前堆栈帧并重新抛出相同的异常. 当 Java 虚拟机弹出当前堆栈帧时, 它有效地中止当前方法的执行并返回调用此方法的方法. 但是, 不是在前一个方法中继续正常执行, 而是在该方法中抛出相同的异常, 这会导致 Java 虚拟机经历搜索该方法的异常表的相同过程.
Java 程序员可以使用 throw 语句抛出异常, 例如 remainder 中的一个子句 catch( ArithmeticException ), 其中一个 DivideByZeroException 创建并抛出. 执行抛出的字节码如下表所示:
athrow 指令从堆栈中弹出顶部字节, 并且会认为它是一个 Throwable 子类的引用(或 Throwable 本身). 抛出的异常是弹出对象引用定义的类型.
Play Ball!: a Java virtual machine simulation
下面的 applet 演示了一个执行一系列字节码的 Java 虚拟机. 模拟中的字节码序列由 javac 生成.
类的 playBall 方法如下所示:
- class Ball extends Exception {
- }
- class Pitcher {
- private static Ball ball = new Ball();
- static void playBall() {
- int i = 0;
- while (true) {
- try {
- if (i % 4 == 3) {
- throw ball;
- }
- ++i;
- }
- catch (Ball b) {
- i = 0;
- }
- }
- }
- }
javac 为该 playBall 方法生成的字节码如下所示:
- 0 iconst_0 // Push constant 0
- 1 istore_0 // Pop into local var 0: int i = 0;
- // The try block starts here (see exception table, below).
- 2 iload_0 // Push local var 0
- 3 iconst_4 // Push constant 4
- 4 irem // Calc remainder of top two operands
- 5 iconst_3 // Push constant 3
- 6 if_icmpne 13 // Jump if remainder not equal to 3: if (i % 4 == 3) {
- // Push the static field at constant pool location #5,
- // which is the Ball exception itching to be thrown
- 9 getstatic #5 <Field Pitcher.ball LBall;>
- 12 athrow // Heave it home: throw ball;
- 13 iinc 0 1 // Increment the int at local var 0 by 1: ++i;
- // The try block ends here (see exception table, below).
- 16 goto 2 // jump always back to 2: while (true) {}
- // The following bytecodes implement the catch clause:
- 19 pop // Pop the exception reference because it is unused
- 20 iconst_0 // Push constant 0
- 21 istore_0 // Pop into local var 0: i = 0;
- 22 goto 2 // Jump always back to 2: while (true) {}
- Exception table:
- from to target type
- 2 16 19 <Class Ball>
该 playball 方法永远循环. 每四次循环, playball 抛出 Ball 并抓住它, 只是因为它很有趣. 因为 try 块和 catch 子句都在无限循环中, 所以乐趣永远不会停止. 局部变量 i 从 0 开始, 每次递增递增循环. 当 if 语句出现 true 时, 每次 i 等于 3 时都会发生 Ball 异常, 抛出异常.
Java 虚拟机检查异常表并发现确实存在适用的条目. 条目的有效范围是 2 到 15(包括两者), 异常在 pc 偏移 12 处抛出. 条目捕获的异常是类 Ball , 抛出的异常是类 Ball. 鉴于这种完美匹配, Java 虚拟机将抛出的异常对象推送到堆栈, 并继续在 pc 偏移 19 处执行 catch 子句, 这里仅将 int i 重置为 0, 并且循环重新开始.
要驱动模拟, 只需按 "步骤" 按钮. 每次按下 "Step" 按钮都会使 Java 虚拟机执行一个字节码指令. 要开始模拟, 请按 "重置" 按钮. 要使 Java 虚拟机重复执行字节码而不需要进一步操作, 请按 "运行" 按钮. 然后, Java 虚拟机将执行字节码, 直到按下 "停止" 按钮. applet 底部的文本区域描述了要执行的下一条指令. 快乐点击.
写在最后:
既然看到这里了, 觉得笔者写的还不错的就点个赞, 加个关注呗! 点关注, 不迷路, 持续更新!!!
来源: http://www.bubuko.com/infodetail-2980549.html