是什么

当程序违反 Java的语法约束时,JVM会向程序标识该错误为异常。一些编程语言通过强制终止程序来对此类错误做出反应。另外一些编程语言允许实现以任意或不可预测的方式做出反应。这两种方法都与 Java提供可移植性和健壮性的设计目标不相符。
不同的是,Java规定在违反语法约束时将抛出异常,并将导致从发生异常的点控制权转移到由程序员指定的点。异常是从它发生的地方抛出,在控制权转移到的点上被捕获

异常类

所有的异常都通过异常类对象来进行处理,其作用是将异常信息从异常点传送给处理点。所有异常类继承自Throwable类。其直接子类有ExceptionError两类。

  • Exception是程序可能希望从中恢复的所有异常的父类。RuntimeExceptionException的直接子类。 RuntimeException是表达式计算期间因各种原因引发但仍可以从中恢复的所有异常的父类。RuntimeException和它的所有子类统称为运行时异常类。
  • Error是程序不指望从中恢复的异常的父类。Error及其所有子类统称为错误类。

运行时异常类和错误类属于非受检(unchecked)异常,其他所有异常均属于受检(checked)异常(包括Throwable),受检异常在编译时会进行异常检查以保证对这些异常进行处理。

不同于Exception异常可以通过catch(Exception e)的方式进行捕获,Error类是那些不太可能恢复的异常。

所有的异常都可以被catch所捕获,对于非受检异常,抛出不需要用throws语句声明。但是一般而言,我们不会主动抛出非受检异常,也不主动对非受检异常进行处理,因为这些异常可能发生在任何地方。

注意Throwable的子类不能被泛化。 异常 - 图1

异常原因

  • 执行throw语句,主动抛出异常,如throw new FileNotFoundException()
  • JVM执行时检测出的异常
    • 表达式违反了Java的正常语义,例如整数除零
    • 程序在加载、链接或初始化部分出错,将抛出LinkageError异常
    • 内部错误或资源限制阻止JVM正常运行,将抛出VirtualMachineError异常

这些异常并不是在程序的任意地方抛出的,在抛出它们的地方,它们被当作表达式计算或语句执行时可能产生的结果,而是在为表达式求值或语句执行时发生。

  • 异步异常

    异步异常

    大多数异常都是同步发生的,这是发生异常的线程的操作的结果,往往是其中某一点导致的这类异常。而异步异常是一种可能在程序执行过程中的任何时候发生的异常。
    异步异常仅在以下情况下发生:

  • 调用类ThreadThreadGroupstop方法(已弃用),一个线程可以调用stop方法来影响另一个线程或特定线程组中的所有线程。它们是异步的,因为它们可能发生在其他一个或多个线程的执行过程中的任何时刻。

  • 内部错误或资源限制阻止JVM正常运行,将抛出VirtualMachineError异常
    • 请注意,StackOverflowErrorVirtualMachineError的子类,它可以通过方法调用同步抛出,也可以由native方法执行或JVM资源限制而被异步抛出。
    • 类似的,OutOfMemeryError,也是VirtualMachineError的子类,在类实例创建、数组创建、类初始化和装箱转换时,可以同步或异步抛出。

Java允许在抛出异步异常之前执行少量但有限制的操作。
异步异常很少见,但如果要生成高质量的机器代码,就必须正确理解它们的语义。
这种异步的延迟使得优化过的代码即使在实际处理时即使符合Java语义(上面提到引发异常是因为不符合java语义规范),也可以检测并抛出这些异常。一个简单的实现是在每个控制转移指令点轮询这种异步异常,由于程序的大小是有限的,因此用于检测异步异常的延迟也是有限的。因为在两个控制转移指令之间不会有任何异步异常,字节码生成器就可以灵活的对这中间的指令进行指令重排,以此获得更高的性能。
建议进一步阅读1993年丹麦哥本哈根函数式编程与计算机体系结构会议论文 Polling Efficiently on Stock Hardware by Marc Feeley, Proc.
Polling Efficiently on Stock Hardware.pdf

编译时异常检查

Java要求程序提供对由方法或构造函数的执行而导致受检(checked)异常的处理机制。编译时异常检查就是检查是否为这些异常进行处理,以此在减少未正确处理的异常数量。对于每个可能产生的受检异常,方法和构造函数的throws语句必须提及该异常类或是该异常类的某个父类。throws语句中指定的受检异常类是实现者和调用者之间的约定。
重写方法的throws子句不能抛出被重写方法中没有声明的受检异常,但是允许抛出非受检异常,即错误类和运行时异常类。 示例,继承重写异常 image.png

  1. public class Father{
  2. public void testExc() throws ClassNotFoundException{
  3. throw new ClassNotFoundException("test");
  4. }
  5. }
  6. class Son extends Father{
  7. @Override
  8. public void testExc() throws IOException, ClassNotFoundException {
  9. // 此处 throws IOException会报错,因为父类该方法只允许抛出ClassNotFoundException
  10. // 而重写的方法就只能抛出ClassNotFoundException及其子类异常
  11. throw new IOException("test");
  12. }
  13. public static void main(String[] args) {
  14. Son son = new Son();
  15. try {
  16. son.testExc();
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. }

当涉及到接口时,因为类可以实现多个接口,而多个接口可能有同名方法,但是这些同名方法可能抛出不一样的异常,那么重写方法throws声明的异常需要与所有相关接口中声明的异常兼容。 示例,多接口重写异常异常 - 图3```java class ExceptionA extends Exception{ public ExceptionA(String msg){ super(msg); } } class ExceptionB extends ExceptionA{ public ExceptionB(String msg){ super(msg); } } class ExceptionC extends ExceptionA{ public ExceptionC(String msg){ super(msg); } } interface IA{ void test() throws ExceptionA; } interface IB extends IA{ void test() throws ExceptionB; } interface IC extends IA{ void test() throws ExceptionC; }

  1. ```java
  2. class Test implements IC, IB {
  3. @Override
  4. public void test() throws ExceptionC, ExceptionB {
  5. // 此时会报错,
  6. // 因为对于IB的test(),并未抛出ExceptionC
  7. // 然而对于IC的test(),并未抛出ExceptionB
  8. // 在这种情况下,没有合适的写法
  9. throw new ExceptionC("test");
  10. // throw new ExceptionB("test");
  11. }
  12. }
  1. class Test implements IA, IB {
  2. @Override
  3. public void test() throws ExceptionB {
  4. // 此时不会报错
  5. // 对于IA的test(),抛出ExceptionA,重写方法抛出的ExceptionB是ExceptionA的子类,合法
  6. // 对于IB的test(),抛出ExceptionB,重写方法也抛出ExceptionB,合法
  7. // 若重写方法声明 throws ExceptionA则会报错
  8. // 虽然满足IA的test(),但是IB抛出的是其子类ExceptionB,不合法
  9. // 这里体现的是多态性
  10. throw new ExceptionB("test");
  11. }
  12. }

非受检异常类不需要进行编译时检查。

  • 错误类无需编译时检查,因为它们可能发生在程序中的多个地方,而且很难甚至不可能从中恢复。程序声明这类异常将显得杂乱无章,毫无意义。只有那种特别精密复杂高要求的程序可能仍希望捕获并尝试从这些情况中恢复。
  • 运行时异常类无需编译时检查,因为按Java设计者的判断,强制要求声明这类异常没什么用。即使这对程序员来可能是显而易见的,但凭Java编译器可用的信息以及编译器执行的分析级别通常不足以确定不会发生此类运行时异常。而且要求声明此类异常只会激怒程序员🤣。

例如,某些代码可能实现循环数据结构,这种结构永远不会导致空引用。程序员可以确定不会发生NullPointerException,但Java编译器很难证明这一点。建立数据结构的这种全局属性所需的定理证明技术超出了本规范的范围。


如果根据下面两小节(表达式异常分析语句异常分析)中提到的规则,语句或表达式的执行可能导致抛出E类异常,我么就说则该语句或表达式可以抛出E类异常。
catch子句可以捕获那些可捕获的异常类:
catch子句的可捕获异常类是其参数声明中指定的异常类。

  1. try{
  2. // 可能会发生异常的语句
  3. } catch (IOException e) {
  4. // 调用方法methodA处理
  5. } catch (ParseException e) {
  6. // 调用方法methodA处理
  7. }

Java7中引入多异常处理,多catch子句的可捕获异常类是其参数声明中指定的多个异常类其中之一,用|分隔。

  1. try{
  2. // 可能会发生异常的语句
  3. } catch (IOException | ParseException e) {
  4. // 调用方法methodA处理
  5. }

表达式异常分析

表达式异常分析在以下情况之一,类实例创建表达式可以抛出异常E

  • 表达式是限定的类实例创建表达式,且限定表达式可以抛出E;
  • 参数列表的某个表达式可以抛出E
  • E是所选构造函数调用类型的异常类型之一
  • 类实例创建表达式包括一个类主体,类主体中的某个实例初始值设定项或实例变量初始值设定项可以抛出E

在以下情况之一,方法调用表达式可以抛出异常E

  • 方法调用表达式的形式为 _Primary_ . _[TypeArguments]_ _Identifier_并且其Primary表达式可以抛出E
  • 参数列表的某些表达式可以抛出E
  • E是所选方法调用类型的异常类型之一

lambda表达式不能抛出任何异常类。
对于其他类型的表达式,当该表达式的某个直接子表达式可以抛出E时,那么该表达式可以抛出一个异常类E。
注意,如果Primary子表达式可以抛出异常类,那么形式为Primary::[TypeArguments]Identifier的方法引用表达式可以抛出异常类。相反,lambda表达式不能抛出任何异常,也没有需要对其执行异常分析的直接子表达式。包含表达式和语句的lambda表达式体可以抛出异常类。

语句异常分析

语句异常分析对于一个 throw语句,如果其抛出表达式具有静态类型E且不是final或有效final的异常参数,那么它可以抛出E或抛出该表达式可以抛出的任何异常类。
例如语句_throw new java.io.FileNotFoundException();_只能抛出_java.io.FileNotFoundException_从形式上来看,这不属于它“可以抛出”FileNotFoundException的子类或父类的情况。
对于一个throw语句,抛出表达式是catch子句C的final或有效的final异常参数,那么它可以抛出异常类E,当且仅当(满足以下所有条件)

  • E是声明C的try语句的try块可以抛出的异常类
  • E与C的任何可捕获异常类兼容的赋值;
  • E与在同一try语句中声明在C前面的catch子句的任何可捕获异常类都不兼容赋值。

异常 - 图4
try语句可以抛出异常类E,当且仅当(至少满足一个条件)

  • try块可以抛出E,或者用来初始化资源的表达式可以抛出E(try-with-resource写法),或者资源的close()方法的自动调用可以抛出E,且E与该try语句的任何catch子句可捕获的异常类都是不兼容的,同时该try语句没有finally块,或finally块可以正常结束
  • try块的某个catch块可以抛出E,且try语句没有finally块,或finally块可以正常结束
  • finally块可以抛出E

显式构造调用语句可以抛出异常E,当且仅当(至少满足一个条件)

  • 构造器调用的参数列表中的某个表达式可以抛出E
  • 被调用的构造器的throws子句声明了异常类E
    1. class Test{
    2. public Test() throws E {
    3. throw new E("test");
    4. }
    5. }
    任何其他类型语句S可以抛出异常E,当且仅当一个直接包含于S的表达式或语句能抛出异常E
    异常检查
    如果方法或构造器可以抛出某个受检异常类E,且E在该方法或构造器中并未用throws进行声明,这就是一个编译时错误。
    1. public void test() throws IOException {
    2. // 合法
    3. throw new FileNotFoundException("test");
    4. // 编译时错误
    5. throw new ClassNotFoundException("test");
    6. }
    如果lambda体可以抛出某个受检异常类E,且E不是lambda表达式所定向的函数类型的throws子句中声明的某个子类,这就是一个编译时错误。
    如果具名类或接口的类变量初始化器或静态初始化器可抛出受检异常,这就是一个编译时错误。
    如果具名类的实例变量初始化器或实例初始化器可以抛出受检异常类,那么除非该异常类或其某个父类显式地在每个构造器的throws子句中都声明了这个异常类,并且该类至少有一个显示声明的构造器,否则,这就是一个编译时错误。
    注意:如果匿名类的实例变量初始化器或实例初始化器可以抛出异常类,那么没有任何编译时错误是因为这个事实而产生的(即这不是编译时错误)。在具名类中,程序员应该负责传播有关那些异常类可以被初始化器抛出的信息,方法是通过在显式的构造器声明中声明合适的throws子句。类的初始化器抛出的受检异常类和类的构造器声明的受检异常类之间的关系对匿名类而言是隐式保证的,因为该类没有任何显示的构造器声明,并且Java编译器总是会为匿名类声明在其初始化器可以抛出的受检异常类的基础上,声明一个带有合适的throws子句的构造器。
    如果catch子句可以捕获异常类E,且非这种情况:与该catch子句相应的try块可以抛出受检异常类E的子类或父类,那么这就是一个编译时错误。除非E是Eception或Exception的父类。
    如果catch子句可以捕获异常类E,并且直接包围它的try语句中之前的catch子句可以捕获E或E的超类,那么这就是一个编译时错误。
    我们鼓励Java编译器在这种情况下产生警告,catch子句可以捕获受检异常类E1,且该catch子句相应的try可以抛出异常类E2,且E2 < E1,且直接包围它的try语句中之前的try块可以捕获受检异常类E3,且E2<E3<E1. ```java import java.io.*;

class StaticallyThrownExceptionsIncludeSubtypes { public static void main(String[] args) { try { throw new FileNotFoundException(); } catch (IOException ioe) { // “catch IOException” catches IOException // and any subtype. }

  1. try {
  2. throw new FileNotFoundException();
  3. // Statement "can throw" FileNotFoundException.
  4. // It is not the case that statement "can throw"
  5. // a subtype or supertype of FileNotFoundException.
  6. } catch (FileNotFoundException fnfe) {
  7. // ... Handle exception ...
  8. } catch (IOException ioe) {
  9. // Legal, but compilers are encouraged to give
  10. // warnings as of Java SE 7, because all subtypes of
  11. // IOException that the try block "can throw" have
  12. // already been caught by the prior catch clause.
  13. }
  14. try {
  15. m();
  16. // m's declaration says "throws IOException", so
  17. // m "can throw" IOException. It is not the case
  18. // that m "can throw" a subtype or supertype of
  19. // IOException (e.g. Exception).
  20. } catch (FileNotFoundException fnfe) {
  21. // Legal, because the dynamic type of the exception
  22. // might be FileNotFoundException.
  23. } catch (IOException ioe) {
  24. // Legal, because the dynamic type of the exception
  25. // might be a different subtype of IOException.
  26. } catch (Throwable t) {
  27. // Can always catch Throwable.
  28. }
  29. }
  30. static void m() throws IOException {
  31. throw new FileNotFoundException();
  32. }

}

  1. 按照上面的规则,在多catch子句中,每个catch子句都必须能捕获try块抛出的但是之前的catch未捕获的某种异常类。
  2. ```java
  3. try{
  4. }catch (FooException e){
  5. }catch (BarException | SubClassOfFooException e){
  6. // 将会导致编译错误,因为在前一个catch中已经处理了FooException
  7. }

运行时异常处理

当一个异常被抛出来时,控制权从引起异常的点转移到最近的动态包围它的try语句的catch子句中,该子句能处理这个异常。
一条语句或一个表达式被catch子句动态包围,是指该语句或表达式出现在try语句的try块内,该catch子句时该try语句的一部分,或者该语句或表达式的调用者被该catch子句动态包围。
一条语句或表达式的调用者依赖于它出现的位置:

  • 如果在一个方法内,那么调用者就是执行时导致该方法被调用的方法调用表达式。
  • 如果在一个构造器、实例初始化构造器或实例变量初始化器内,那么调用者就是使用类或接口导致其被初始化的表达式。

特定的catch能否处理异常取决于它可捕获的类与被抛出异常类的关系。如果catch子句可捕获的异常类之一是所抛出异常类或是所抛异常类的父类,那么该catch子句就可以处理这个异常。(简单点就是如果抛出异常类的实例用instanceof判断与catch声明的异常类之一为true那就是可处理)
当一个异常被抛出来时,控制权从引起异常的点转移到异常处理处,而引发异常的代码不再继续执行。
所有异常都必须很精确(无论是异步还是同步):当控制权发生转移时,必须保证该点(异常抛出点)之前语句执行和表达式执行的效果确实产生,该点之后的语句、表达式等其他部分即使执行了也必须回退,像是未执行一样。
如果经编译器优化过的代码通过指令重排执行了异常点之后的语句或表达式,那么这种优化代码的结果应该对用户不可见,就像未优化一样。
如果没有找到任何能处理某个异常的catch子句,那么当前线程将终止。终止前将执行所有的finally子句,并会根据以下规则处理未捕获异常:

  • 如果当前线程设置了未捕获异常处理机制,则执行该异常处理机制
  • 否则为当前线程的父对象ThreadGroup调用uncaughtEception方法。如果该ThreadGroup及其所有的ThreadGroup父对象都没有重写uncaughtEception方法,那么缺省处理器的uncaughtEception方法就会被调用。

如果希望可以确保某个代码块总是在另一个代码块之后执行,即使代码块会突然结束也是如此,那可以使用带有finally子句就会在异常传播过程中执行,即使最终没有找到任何匹配的catch子句也是如此。
如果执行finally子句是因为try块突然结束,且finally自身也突然结束了,那么try块异常结束的原因就会被finally块的异常覆盖。
突然结束和异常捕获的准确规则在14与15章特别是15.6进行详细说明

  1. class TestException extends Exception {
  2. TestException() { super(); }
  3. TestException(String s) { super(s); }
  4. }
  5. class Test {
  6. public static void main(String[] args) {
  7. for (String arg : args) {
  8. try {
  9. thrower(arg);
  10. System.out.println("Test \"" + arg +
  11. "\" didn't throw an exception");
  12. } catch (Exception e) {
  13. System.out.println("Test \"" + arg +
  14. "\" threw a " + e.getClass() +
  15. "\n with message: " +
  16. e.getMessage());
  17. }
  18. }
  19. }
  20. static int thrower(String s) throws TestException {
  21. try {
  22. if (s.equals("divide")) {
  23. int i = 0;
  24. return i/i;
  25. }
  26. if (s.equals("null")) {
  27. s = null;
  28. return s.length();
  29. }
  30. if (s.equals("test")) {
  31. throw new TestException("Test message");
  32. }
  33. return 0;
  34. } finally {
  35. System.out.println("[thrower(\"" + s + "\") done]");
  36. }
  37. }
  38. }
  1. divide null not test
  1. [thrower("divide") done]
  2. Test "divide" threw a class java.lang.ArithmeticException
  3. with message: / by zero
  4. [thrower("null") done]
  5. Test "null" threw a class java.lang.NullPointerException
  6. with message: null
  7. [thrower("not") done]
  8. Test "not" didn't throw an exception
  9. [thrower("test") done]
  10. Test "test" threw a class TestException
  11. with message: Test message

thrower方法声明必须有一个throws子句,因为它抛出TestException,这是受检异常类。如果没有throws子句,就会产生编译时错误。注意,finally子句是在每次调用thrower时执行,无论是否产生异常。