Skip to content

Latest commit

 

History

History
235 lines (122 loc) · 71.6 KB

11-异常.md

File metadata and controls

235 lines (122 loc) · 71.6 KB

第十一章

当程序违反 Java 编程语言的语义约束时,Java 虚拟机将此错误作为异常发送给程序。

此类违反的一个示例是,试图在数组边界之外进行索引。一些程序语言和它们的实现通过不同分说地终止程序来对此类错误作出反应;其它程序语言允许实现以任意的或不可预知的方式作出反应。这些方法都不兼容 Java SE 平台的设计目标:提供可移植性和健壮性。

相反,Java 编程语言指定,当违反语义约束时,抛出异常,并导致从异常发生的点到由程序员指定的点的非局部控制转移。

异常被称为从其发生的点被抛出,并被称为在控制被转移的点处被捕获。

程序也可以显式地抛出异常,使用 throw 语句(14.18)。

throw 语句的显式使用提供了通过返回古怪的值的老式的处理错误条件风格的替代,例如整数值 -1,其中负数值不是正常情况所期望的。经验显示,太多此类值被调用者忽略或不检查,导致程序不健壮,呈现出出乎意料的行为,或两者都有。

每个异常由类 Throwable 或其子类之一(11.1)的实例表示。此类对象可以用于向捕获此异常的处理器携带来自异常发生点处的信息。处理器通过 try 语句(14.20)的 catch 子句建立。

在抛出异常的过程中,Java 虚拟机突然完成,一个接一个,任何在当前线程已经开始的但未完成执行的表达式、语句、方法和构造器调用、初始化器和字段初始化表达式。此过程将继续,直到找到一个处理器,该处理器表示它通过命名该异常的类或该异常的类的父类来处理该特定的异常。如果未找到此类异常,则可以通过未捕获异常处理器(11.3)的层次结构之一来处理该异常 - 因此,尽一切努力避免让异常变成未经处理的。

Java SE 平台的异常机制与它的同步模型(17.1)集成在一起,从而在同步语句(14.19)和同步方法(8.4.3.6,15.12)的调用突然完成时,解锁管程。

11.1. 异常的种类和原因

11.1.1. 异常的种类

异常由类 Throwable(Object 的直接子类)或其子类之一的实例表示。

Throwable 和其所有子类合称异常类。

类 Exception 和 Error 是 Throwable 的直接子类:

    * Exception 是普通程序希望从中恢复的异常的父类。

      RuntimeException 是 Exception 的直接子类。RuntimeException 是表达式计算期间因各种理由抛出的所有异常的父类,但可以从中恢复。

      RuntimeException 和所有其子类合称运行时异常类。

    * Error 是普通程序一般不希望从中恢复的所有异常的父类。

      Error 和其所有子类合称错误类。

未检查异常类是运行时异常类和错误类。

已检查异常类是除未检查异常类之外的所有异常类。即,已检查异常类是除 RuntimeException 和子类以及 Error 和其子类之外的 Throwable 和其所有子类。

程序可以在 throw 语句中使用 Java SE 平台的预定义的异常类,或根据需要将其它异常类定义为 Throwable 或其任何子类的子类。为了充分利用异常处理器(11.2)的编译时检查,通常将大多数新异常类定义为已检查异常类,即,Exception 的子类,其不是 RuntimeException 的子类。

类 Error 是 Throwable 的单独子类,不同于类层次结构中的 Exception,以允许程序使用惯用语法 "} catch (Exception e) {"(11.2.3)来捕获可以从中恢复的所有异常,而不是捕获通常不能从中恢复的错误。

注意,Throwable 的子类不能是泛型的(8.1.2)。

11.1.2. 异常的原因

异常因三个原因之一抛出:

    * 执行 throw 语句(14.18)。

    * 由 Java 虚拟机同步地检测出的异常的执行条件,即:

        表达式计算违反了 Java 编程语言(15.6)的语义,例如整数除以零。

        在程序的加载、连接或初始化部分(12.2,12.3,12.4)发生的错误;在这种情况下,抛出 LinkageError 的子类的实例。

        内部错误或资源限制阻止 Java 虚拟机实现 Java 编程语言的语义;在这种情况下,抛出 VirtualMachineError 的子类的实例。

      这些异常不是在程序的任意点处抛出,而是将它们指定为表达式计算或语句执行的可能结果的点处。

    * 发生异步的异常(11.1.3)。

11.1.3. 异步的异常

大多数异常作为在它们发生的线程中的操作的结果同步地发生,并在指定为可能导致此类异常的程序中的某个点上。相比之下,异步异常是在程序执行中的任何点处可能发生的异常。

异步异常仅作为以下结果发生:

    * 类 Thread 或 ThreadGroup 的(舍弃的)stop 方法的调用。

      (舍弃的)strop 方法可能被一个线程调用,以影响另一个线程或指定线程组中所有线程。它们是异步的,因为它们可以在其它线程执行的任何点处发生。

    * Java 虚拟机中阻止其实现 Java 编程语言的语义的内部错误或资源限制。在这种情况下,抛出的异步异常是 VirtualMachineError 的子类的实例。

      注意,StackOverflowError,VirtualMachineError 的子类,可以由方法调用(15.12.4.5)同步地,也可以由于 native 方法执行或 Java 虚拟机资源限制异步地,抛出。类似地,OutOfMemoryError,VirtualMachineError 的另一个子类,可以在类实例创建(15.9.4,12.5)、数组创建(15.10.2,10.6)、类初始化(12.4.2)和装箱转换(5.1.7)期间同步地,也可以异步地,抛出。

Java SE 平台允许在抛出异步异常之前发生一段小的有限的执行。

异步异常是罕见的,,但正确的理解它们的语义是必须的,如果想生成高质量的机器码。

以上提到的延迟允许,在遵守 Java 编程语言的语义的同时,检测优化的代码并在实际处理这些异常的点处抛出这些异常。简单的实现可能会在每个控制转义智力的点处对异步异常进行轮询。由于程序有有限的大小,因此在检测异步异常时,这将提供对总延迟的绑定。由于控制转义之间不会发生异步异常,因此代码生成器具有一定的灵活性,可以在控制转义之间的重排序计算以获得更好的性能。建议建议不阅读论文, Polling Efficiently on Stock Hardware by Marc Feeley, Proc. 1993 Conference on Functional Programming and Computer Architecture, Copenhagen, Denmark, pp. 179-187。

11.2. 异常的编译时检查

Java 编程语言要求,程序包含可以产生自方法或构造器(8.4.6,8.8.5)的执行的已检查异常的处理器。此编译时检查的异常处理器的存在旨在减少未正确处理的异常的数量。对于每个是可能结果的已检查异常,方法或构造器的 throws 子句必须提及该异常的类或该异常(11.2.3)类的父类之一。

在 throws 子句中命名的已检查异常类(11.1.1)是方法或构造器的实现者和用户之间的约定的一部分。重写方法的 throws 子句不能指定,此方法将导致抛出被重写的方法不允许的任何已检查异常,通过其 throws 子句抛出的(8.4.8.3)。当涉及接口时,单个重写声明可能重写多于一个方法声明。在这种情况下,重写声明必须具有与所有被重写的声明(9.4.1)兼容的 throws 子句。

未检查异常类(11.1.1)免除编译时检查。

免除 Error 类,因为它们可以在程序中的许多点处发生,从其中恢复是困难的或不可能的。声明此类异常的程序是杂乱的、毫无意义的。复杂的程序可能还希望捕获并试图从这些条件的一些恢复。

免除运行时异常类,因为在 Java 编程语言设计者的判断中,必须声明此类异常不会有助于确立程序的正确性。Java 编程语言的许多操作和结构可能在运行时导致异常。Java 编译器可用的信息以及编译器执行的分析级别通常不足以确定此类运行时异常不会发生,即使这对程序员来说是明显的。要求声明此类异常对程序员来说是意见恼火的事。

例如,特定代码可能通过构造实现从不涉及 null 引用的循环数据结构。然后程序员可以确定,NullPointerException 不会发生,但 Java 编译器很难证明它。建立数据结构的这种全局属性所需的定理证明技术超出了本规范的范围。

如果,根据 11.2.1 和 11.2.2 中的规则,语句或表达式的执行可以导致类 E 的异常被抛出,则我们说此语句或表达式可以抛出异常类 E。

我们说 catch 子句可以捕获它的可捕获的异常类:

    * 单 catch 子句的可捕获的异常类是其异常参数(14.20)的声明类型。

    * 多 catch 子句的可捕获的异常类是在表示其异常参数的类型的单元中的选项。

11.2.1. 表达式的异常分析

类实例创建表达式(15.9)可以抛出异常类 E,当且仅当:

    * 这些表达式是限定的类实例创建表达式,并且是有资格抛出 E 的表达式;或

    * 参数列表的某些表达式可以抛出 E;或

    * E 是选定的构造器(15.12.2.6)的调用类型的异常类型之一;或

    * 类实例创建表达式包含 ClassBody,ClassBody 中的某些实例初始化器或实例变量初始化器可以抛出 E。

方法调用表达式(15.12)可以抛出异常类 E,当且仅当:

    * 方法调用表达式的形式是 Primary . [TypeArguments] Identifier,并且 Primary 表达式可以抛出 E;或

    * 参数列表的某些表达式可以抛出 E;或

    * E 是选定的方法(15.12.2.6)的调用类型的异常类型之一。

lambda 表达式(15.27)不可以抛出异常类。

对于每个其它种类的表达式,表达式可以抛出异常类 E,当且仅当其直接子表达式之一可以抛出 E。

注意,形式 Primary :: [TypeArguments] Identifier 的方法引用表达式(15.13)可以抛出异常类,如果 Primary 子表达式可以抛出异常类。相比之下,lambda 表达式什么也不能抛出,并且没有可以执行异常分析的直接子表达式。而 lambda 表达式的 body,包含可以抛出异常类的表达式和语句。

11.2.2. 语句的异常分析

throw 语句(14.18),其被抛出的表达式具有 static 类型 E 且不是 fianl 或有效地 final 的异常参数,可以抛出被抛出的表达式可以抛出的 E 或任何异常类。

例如,语句 throw new java.io.FileNotFoundException(); 仅可以抛出 java.io.FileNotFoundException。正是地,它不是“可以抛出”java.io.FileNotFoundException 的子类或父类的情况。

throw 语句,其被抛出的表达式是 catch 子句 C 的 final 或有效地 final 的异常参数,可以抛出异常类 E,当且仅当:

    * E 是声明 C 的 try 语句的 try 块可以抛出的异常类;并且,

    * E 与 C 的任何可捕获的异常类是赋值兼容的;并且

    * E 与 同一 try 语句中声明在 C 的左侧的 catch 子句的任何可捕获的异常类不是赋值兼容的。

try 语句(14.20)可以抛出异常类 E,当且仅当:

    * try 块可以抛出 E,或用于初始化资源(在 try-with-resources 语句中)的表达式可以抛出 E,或资源(在 try-with-resources 语句中)的 close() 方法的调用可以抛出 E,并且 E 与 try 语句的任何 catch 子句的任何可捕获的异常类不是赋值兼容的,并且不存在 finally 块或 finally 块可以正常完成;或

    * 某些 try 语句的 catch 块可以抛出 E,并且不存在 finally 块或 finally 块可以正常完成;或

    * 存在 finally 块且可以抛出 E。

显式的构造器调用语句(8.8.7.1)可以抛出异常类 E,当且仅当:

    * 构造器调用的参数列表的某些表达式可以抛出 E;或

    * E 被确定是被调用(15.12.2.6)的构造器的 throws 子句的异常类。

任何其它语句 S 可以抛出异常类 E,当且仅当直接包含在 S 中的表达式或语句可以抛出 E。

11.2.3. 异常检查

这是一个编译时错误,如果方法或构造器 body 可以抛出某些异常类 E,当 E 是已检查的异常类且不是在方法或构造器的 throws 子句中声明的某些类的子类。

这是一个编译时错误,如果 lambda body 可以抛出某些异常类 E,当 E 是已检查的异常且不是在由 lambda 表达式作为目标的函数类型的 throws 子句中声明的某些类的子类。

这是一个编译时错误,如果命名的类或接口的类变量初始化器(8.3.2)或 static 初始化器(8.7)可以抛出已检查的异常类。

这是一个编译时错误,如果命名的类的实例变量初始化器(8.3.2)或实例初始化器(8.6)可以抛出已检查的异常类,除非此命名的类具有至少一个显式声明的构造器,并且异常类或其父类之一是在每个构造器的 throws 子句中显式地声明的。

注意,如果匿名类(15.9.5)的实例变量初始化器或实例初始化器可以抛出异常类,则不会导致编译时错误。在命名类中,程序员有责任通过在任何显式的构造器声明上声明适当的 throws 子句,来传播初始化器可以抛出哪些异常类的信息。由类的初始化器抛出的已检查异常类和由类的构造器声明的已检查异常类之间的这种关系是匿名类声明的保证,因为不可能有显式的构造器声明,并且 Java 编译器总是基于它的初始化器可以抛出的已检查异常类为匿名类声明具有适当 throws 子句的构造器。

这是一个编译时错误,如果 catch 子句可以捕获已检查异常类 E1,并且它不是对应此 catch 子句的 try 块可以抛出已检查异常类,该已检查异常类是子类或 E1 的父类,除非 E1 是 Exception 或 Exception 的父类。

鼓励 Java 编译器发出警告,如果 catch 可以捕获已检查异常类 E1,并且对应此 catch 子句的 try 块可以抛出已检查异常类 E2,其中 E2 <: E1,并且直接封闭 try 语句的 catch 子句前面可以捕获已检查异常类 E3,其中 E2 <: E3 <: E1。

avatar avatar

11.3. 异常的运行时处理

当抛出异常(14.18)时,控制从引发异常的代码转移到可以处理此异常的 try 语句的最接近的动态地封闭 catch 子句,如果有。

语句或表达式是由 catch 子句动态地封闭的,如果它出现在 catch 子句是一部分的 try 语句的 try 块中,或如果语句或表达式的调用者是由 catch 子句动态地封闭的。

语句或表达式的调用者依赖于它出现的位置:

    * 如果在方法中,则调用者是被执行的导致方法被调用的方法调用表达式(15.12)。

    * 如果在构造器或实例初始化器或实例变量的初始化器中,则调用者是被执行的导致对象被创建的类实例创建表达式(15.9)或 newInstance 的方法调用。

    * 如果在 static 初始化器或 static 变量初始化器中,则调用者是使用该类或接口的以便使其被初始化的表达式。

具体的 catch 子句是否可以处理异常,是通过比较被抛出的对象的类和 catch 子句的可捕获的异常类来确定的。catch 子句可以处理异常,如果其可捕获的异常类之一是此异常类或此异常类的父类。

等效地,catch 子句将捕获其可捕获的异常类的 instanceof 之一的任何异常对象。

在异常被抛出时发生的控制转移导致表达式(15.6)和语句(14.1)的突然的完成,直到遇到可以处理此异常的 catch 子句;然后,执行继续,通过执行该 catch 子句的块。导致异常的代码永远不会恢复。

所有异常(同步和异步的)是精确的:当控制转移发生时,在抛出异常的点之前的已执行的语句和已计算的表达式的所有效果必须看起来已经发生。没有在抛出异常的点之后出现的表达式、语句或部分看起来已经被计算。

如果优化后的代码推投机地行了异常发生的点后面的一些表达式或语句,则必须准备此类代码,以便从程序的用户可见状态隐藏此投机的执行。

如果找不到可以处理异常的 catch 子句,则当前线程(遇到异常的线程)被终止。在终止前,执行所有 finally 子句,并且根据以下规则处理未捕获的异常:

    * 如果当前线程有未捕获的异常处理器集合,则执行该处理器。

    * 否则,调用当前线程的父级的 ThroudGroup 的方法 uncaughtException。如果 ThreadGroup 和其父级 ThreadGroups 没有重写 uncaughtException,则调用默认处理器的 uncaughtException。

在想要确保一个代码块总是在另一个之后执行的情况下,即使该其它代码块突然完成,可以使用带有 finally 子句(14.20.2)的 try 语句。

如果由于 try 块的突然完成而执行 finally 子句,并且此 finally 本身也突然完成,则 try 块突然完成的原因被丢弃,并且从这传播新的突然完成原因。

在 14(Blocks and Statements)中的每个语句的规范以及 15(Expressions)(特别是 15.6)中的表达式的详情中,指定了突然完成和异常捕获的准确规则。