Java SE 7 Third Edition 规范翻译之-异常

当一个程序违反了Java语言的语义约束,Java虚拟机将抛出异常。

一个典型的违反约束的例子是企图索引一个越界的数组。一些语言通过强制终止程序来处理这种类型的错误;另一些语言允许这种实现,但处理方式是随意的或者不可预知的。不同于这两种实现,Java SE平台的设计目标是:提供可移值性和健壮性。

Java编程语言指出当语义约束遭到违反时将抛出一个异常,同时程序将指出导致异常的代码,并交出控制权。

异常被认为从导致它的地方抛出,并且在这个地方交出控制权。

程序也可以使用throw语句抛出自定义异常。自定义异常通过返回值来替代老式风格的错误处理机制,例如一个整数不希望出现负值时,经验表明调用者经常忽略了这样的验证,导致程序不健壮,产生不希望的结果,或者两者兼有。

每个异常都是Throwable或者它的子类的一个实例(§11.1)。异常对象可以用来承载异常发生时的信息,提供给处理程序,由try语句抛出,catch语句创建处理程序(§14.20)。

在抛出异常的过程中,Java虚拟机意外中止,一个接一个,任何当前线程已经开始的表达式,语句,方法,构造函数,初始化,属性初始化表达式将挂起,直到找到这个异常处理类或者异常父类的处理类(§11.2)。如果没有对应的处理类,那么会当成未被处理的异常处理,因为所做的努力都是避免让异常没有对应的处理方法。

Java SE平台的异常机制集成了其同步模式(§17.1),使监视器异常中止时开启为同步声明(§14.19)以及同步方法调用(§8.4.3.6, §15.12)

11.1 The Kinds and Causes of Exceptions

11.1.1 The Kinds of Exceptions

一个异常代表Throwable(Object的直接子类)或者它子类的一个实例

Throwable和它所有的子类统称为异常类。需要注意的是Throwable的子类一定是不能通用的(§8.1.2)。

类Exception和Error都是Throwable的直接子类。

Exception是程序希望可以继续运行时抛出的所有异常的父类。

Error是不希望程序继续运行时抛出的所有异常的父类。

Error和它的子类被统称为错误类。

Error类是Throwable的独立的子类,和Exception是两个不同的分支,允许程序使用语法 “} catch (Exception e) {“ (§11.2.3) 捕获所有的异常从而恢复运行是可行的,想通过捕获错误来恢复运行是不可行的。

RuntimeException类是Exception的一个直接子类。

RuntimeException是所有表达式赋值运行时各种异常的父类,但仍可以继续运行。

RuntimeException和它的所有子类统称为运行时异常类。

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

非检查异常类之外的异常都属于检查异常类。也就是说,检查异常类都继承自Throwable类,而不是RuntimeException和它子类或者Error和它子类。

程序可以在throw语句里使用Java SE平台API定义好的异常类,或者自定义的Throwable或者它子类的子类。利用编译时检查异常处理(§11.2),这个是典型的大部分检查异常类的定义方法,换句话说检查异常类是Exception的子类而不是RuntimeException的子类。

11.1.2 The Causes of Exceptions

抛出异常的三个原因:

  • throw语句(§14.18)被执行。
  • 由Java虚拟机同步检测的异常执行情况,即:
    • 运算表达式违反Java程序主义,例如整数除以0。
    • 程序加载,连接,或者初始化部分导致的错误(§12.2, §12.3, §12.4);在这种情况下抛出一个LinkageError子类的实例。
    • 内部错误或者资源访问限制阻止Java虚拟机实现Java程序方法;在这种情况下抛出一个VirtualMethodError子类的实例。

      这些异常不需要在程序里显式的抛出,而是在运算表达式或者语句执行时可能导致的异常。

  • 发生异步异常(§11.1.3)

11.1.3 Asynchronous Exceptions

大部分异常在线程执行时同步抛出,并且指出导致这个异常可能的地方。相比之下异步异常可能发生在程序执行的任何地方。

异步异常仅发生在:

  • 调用线程或者线程组(过期的)stop方法。一个线程调用(过期的)stop方法影响到另外一个线程或者一个特别线程组里面所有的线程。他们是异步的,因为他们可以在其他线程或者线程组里面任何地方执行。
  • Java虚拟机一个内部错误或者资源访问阻止了Java语义的实现。在这种情况下,抛出VirtualMethodError子类的一个实例。

需要注意的是StackOverflowError,VirtualMethodError的一个子类,既可以在方法调用时同步抛出,也可以执行本地方法或者Java虚拟机资源访问限制时异步抛出。同样,OutOfMemoryError,另一个VirtualMethodError的子类,可以在对象创建(§12.5),数组创建(§15.10.1, §10.6),类初始化(§12.4.2),和装箱转换(§5.1.7)期间同步或者异步抛出。

Java SE平台在异步异常抛出前允许一个小的但数量有限的执行。

异步异常是罕见的,但如果要生成高品质的机器代码,正确理解其语义是必要的。

上面提到的延迟允许优化代码,以检测和抛出这些异常点是遵守Java编程语义可行的处理。一个简单的实现可能轮循在每个控制转移指令点的异步异常。因为程序大小有限制,这提供了一个检测异步异常的总延时。因为控制转换不会导致异步异常,代码解释器拥有一定的灵活性,在控制转换时重新排列达到更好的性能。Marc Feeley 在 Proc. 1993 发表的论文 Polling Efficiently on Stock Hardware 讨论了函数式编程和计算机架构,哥本哈根,丹麦,第179-187页,建议详细阅读。

11.2 Compile-Time Checking of Exceptions

Java编程语言要求程序为可能导致异常的执行方法或者构造函数提供检查处理方法,对每个可能的异常检查,方法(§8.4.6)或者构造函数(§8.8.5)中的throws子句必须提到该异常类或者该异常类的一个超类(§11.2.3)。

编译时检查异常处理设计是为了减少不确当处理异常数量。检查在throws子句中指定的异常类(§11.1.1)是方法或者构造函数实现者和使用者之间约定的一部分。重写方法的throws子句可以不用指定此方将导致抛出任何检查异常,不允许在重写方法中通过throws子句抛出异常(§8.4.8.3)。

当涉及到多个接口,多个方法的声明可能被一个方法重写。在这种情况下,重写声明必须有一个throws子句覆盖所有的被重写的声明(§9.4.1)。

非检查异常类(§11.1.1)在编译时不检测。

未检查异常类中的错误类不校验,因为他们可以发生在程序的很多地方,恢复起来很困难或者不可能。声明这种异常将使程序很混乱,毫无意义。复杂的程序可能希望捕获这些异常并企图从其中的一些条件恢复。

未检查异常类中的运行时异常类不校验,因为在Java程序语言的设计者们的判断,申报此类异常不会在建立程序正确性方面有显著的帮助。Java程序的许多操作和构造都能导致运行时异常。Java编译器所拥有的信息,以及分析编译器的水平,通常不足以保证运行时异常不发生,即使对程序员来说是很明显的。需要声明这种异常对程序员来简直是悲剧。

例如,某些代码可能通过构造器执行一个循环的数据结构,不能包含空引用;程序员可以肯定不会发生NullPointerException异常,但对Java编译器来说想要证明它就很困难。想要证明需要建立一个全局的数据结构属性,已经超出了本规范的范围。

我们说如果一个声明或者表达式可能抛出一个检查异常类 E,根据§11.2.1和§11.2.2的规则,声明或者表达式执行可能导致一个异常类 E 被抛出。

我们说一个catch子句可以捕获它对应的异常类。单catch子句需要声明该异常类型参数(§14.20)。

多catch子句用来处理多个异常,每个catch需要指定其对应的异常类型参数(§14.20)。

11.2.1 Exception Analysis of Expressions

一个类实例创建表达式(§15.9)可能抛出异常类 E,仅当:

  • 表达式是一个正确的类实例创建表达式,正确的表达式可能抛出异常 E;或
  • 参数列表的一些表达式可能抛出 E;或者
  • E 在被调用的构造器最后作为异常类通过throws子句抛出(§15.12.2.6);或
  • 类实例创建表达式包含一个类体,类体的一些实例初始化块或者实例变量初始化表达式可能抛出 E。

一个方法调用表达式(§15.12)可能抛出异常类 E,仅当:

  • 方法调用是形式,标签符和主要的表达式可能抛出 E;或
  • 参数列表的一些表达式可能抛出 E;或
  • E 在被调用的构造器最后作为异常类通过throws子句抛出(§15.12.2.6);

对于每一种表达式,仅当其中一个表达式可能抛出异常类 E,表达式才可能抛出 E。

11.2.2 Exception Analysis of Statements

throw语句(§14.18),其thrown表达式具有静态类型 E,并且是非final或者effectively final表达式参数,可以抛出 E 或者任意可以抛出的异常类。

例如,声明抛出new java.io.FileNotFoundException(),只能抛出java.io.FileNotFoundException。形式上来说它不是这样的,它能够抛出java.io.FileNotFoundException的子类或者超类。

throw语句抛出的异常作为catch语句C的一个final和effectively final异常参数,可以抛出异常类 E 仅当:

  • E 是一个异常类,C 声明的try语句的try块中可以抛出;和
  • E 与 C 的任何可捕获的异常类兼容;和
  • E 不能与 C 里面同一个 try 声明对应的 catch 语句任何可捕获的异常类冲突。

一个 try 语句可以抛出异常类 E,仅当:

  • 该 try 块可以抛出 E,或者一个资源初始化表达式(在try-with-resources语句)可以抛出E,或者自动调用资源的close()方法(在try-with-resources语句)可以抛出E,同时 E 与任何该 try 语句对应的的 catch 子句中可捕获的异常类不能冲突,并且没有finally块存在,或者finally块可以正常完成;或
  • try语句的一些catch块可以抛出 E,并且没有finally块存在或者finally块可以正常完成;或
  • finally块存在并且可以抛出E。

一个显式的构造函数调用语句(§8.8.7.1)可以抛出异常类 E,仅当:

  • 构造函数的参数列表的一些表达式可以抛出 E;或
  • 被调用的构造函数内部使用throws子句抛出异常类 E。

任何其他的声明 S 可以抛出异常类 E,仅当 S 直接包含的表达式或者声明可以抛出 E。

11.2.3 Exception Checking

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

这是一个编译时错误,如果一个类变量初始化(§8.3.2),或指定类或接口的静态初始化(§8.7)可以抛出一个检查异常类。

这是一个编译时错误,如果一个实例变量初始化或者指定类的实例变量初始化可以抛出一个检查异常类,除非异常类或者它的超类在其对应类的每个构造函数中throws子句里显式声明,并且该类至少拥有一个显示声明的构造函数。

注意,没有编译时抛出异常类是由于匿名类的变量初始化或者实例初始化引起的。在非匿名类中,程序有义务通过在任何显式的构造函数声明中定义一个合适的throws子句去传播初始化抛出的异常类相关的信息。类初始化抛出的检查异常类和匿名类构造函数隐式初始化声明的检查异常类之间存在一定的联系,因为不可能有明确的显式的构造函数声明,Java编译器总是基于初始化可能抛出的检查异常类,为匿名类声明生成一个拥有合适的throws子句构造函数。

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

这是一个编译时错误,如果一个catch子句可以捕获(§11.2)检查异常类 E1,上述的catch子句可以立即捕获try语句中的 E1 或者 E1 的超类。

Java编译器鼓励发出警告,当catch子句可以捕获(§11.2)检查异常类 E1,并且try块对应的catch子句可能抛出检查异常类 E1 的子类 E2,上述的catch子句可以捕获try语句中的检查异常类 E3,其中 E2 <:E3 <: E1。

Example 11.2.3-1. Catching Checked Exceptions

import java.io.*;
class StaticallyThrownExceptionsIncludeSubtypes {
	public static void main(String[] args) {
		try {
			throw new FileNotFoundException();
		} catch (IOException ioe) {
			// Legal in Java SE 6 and 7. "catch IOException"
			// catches IOException and any subtype.
		}
		try {
			throw new FileNotFoundException();
			// Statement "can throw" FileNotFoundException.
			// It is not the case that statement "can throw"
			// a subtype or supertype of FileNotFoundException.
		} catch (FileNotFoundException fnfe) {
			// Legal in Java SE 6 and 7.
		} catch (IOException ioe) {
			// Legal in Java SE 6 and 7, but compilers are
			// encouraged to throw warnings as of Java SE 7.
			// All subtypes of IOException that the try block
			// can throw have already been caught.
		}
		try {
			m();
			// Method m's declaration says "throws IOException".
			// m "can throw" IOException. It is not the case
			// that m "can throw" a subtype or supertype of
			// IOException, e.g. Exception, though Exception or
			// a supertype of Exception can always be caught.
		} catch (FileNotFoundException fnfe) {
			// Legal in Java SE 6 and 7, because the dynamic type
			// of the IOException might be FileNotFoundException.
		} catch (IOException ioe) {
			// Legal in Java SE 6 and 7.
		} catch (Throwable t) {
			// Legal in Java SE 6 and 7.
		}
	}
	static void m() throws IOException {
		throw new FileNotFoundException();
	}
}

根据上述原则,在多个catch子句(§14.20)中的每一个catch子句必须捕获try块中抛出并且在前面的catch没有被捕获的一些异常。例如,下面的第二个catch子句可能导致一个编译时错误,因为异常分析确定SubclassOfFoo已经被第一个catch子句捕获过了:

try { ... }
catch (Foo f) { ... }
catch (Bar | SubclassOfFoo e) { ... }

11.3 Run-Time Handling of an Exception

当一个异常被抛出(§14.18),控制权从导致异常的代码转移到最近的动态catch子句块,如果有的话,try语句就可以处理该异常。

一个声明或者表达式被一个catch子句动态隔离,当它出现在try语句的try块中的话catch子句就是其中的一部分,或者当声明或者表达式的调用者被catch子句动态隔离。

声明或者表达式的调用者取决于它发生在哪里:

  • 如果在方法中,那么调用者是一个方法调用表达式(§15.12),执行方法调用。
  • 如果在构造函数或者实例初始化或者实例变量初始化时,那么调用者就是类实例创建表达式(§15.9)或者一个新实例的方法调用,执行创建一个新对象。
  • 如果在一个静态的初始化或者一个静态变量初始化时,那么调用者就是使用的类或者接口,以便其初始化(§12.4)。

一个特定的catch子句能否处理一个异常由抛出的对象跟catch子句能捕获的异常类比较决定。catch子句可以处理如果它可捕获的异常类是抛出的异常类或者抛出异常类的父类。

同样的,一个catch子句将会捕获任何其可捕获的异常类的实例(§15.20.2)。

当有异常被抛出时,表达式(§15.6)和声明(§14.1)意外中止,控制权转移,直到遇到一个能处理这个异常的catch子句;继续执行catch块。导致异常的代码将不会恢复执行。

所有的异常(同步或者异步)是精确的:当转换控制发生,在抛出异常之前的所有语句执行和表达式运算都会受到影响。表达式,语句,或其他部分在抛出异常后都不会被执行。

如果优化代码期望在异常发生点之后执行一些表达式或者语句,这样的代码必须准备从程序的用户可见状态隐藏推测执行。

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

  • 如果当前线程有一个未被捕获的异常处理集,那么该处理程序被执行。
  • 否则,当前线程的父线程ThreadGroup将调用方法uncaughtException。如果ThreadGroup和它的父类ThreadGroups没有重写uncaughtException方法,那么默认的处理程序uncaughtException方法将被调用。

在某此情况下可能需要确保一个代码块在另一个代码块之后始终被执行,即使另一个代码块意外中止,可以使用try语句的finally子句(§14.20.2)实现。

如果try-finally或者try-catch-finally语句的一个try块或catch块意外中断,那么finally子句在异常传播的过程中执行,即使最终没有发现匹配的catch子句。

如果一个finally子句由于try块的意外中断而执行,并且finally块自身执行时意外中断,那么try块意外中断的异常被丢弃,一个新的意外中断的异常从这里开始传播。

意外中断和异常捕获的确切规则指定§14中每个语句详细描述以及§15(特别是§15.6)中的异常。

Example 11.3-1. Throwing and Catching Exceptions

下面的程序声明了一个异常类TestException。Test类的main方法调用了thrower四次,导致异常三次被抛出。方法中的try语句捕获了thrower抛出的每一个异常。无论thrower调用正常结束或者异常中断,都会打出一条消息,描述做了什么。

class TestException extends Exception {
	TestException() { super(); }
	TestException(String s) { super(s); }
}
class Test {
	public static void main(String[] args) {
		for (String arg : args) {
			try {
				thrower(arg);
				System.out.println("Test \"" + arg +
				"\" didn't throw an exception");
			} catch (Exception e) {
				System.out.println("Test \"" + arg +
				"\" threw a " + e.getClass() +
				"\n with message: " +
				e.getMessage());
			}
		}
	}
	static int thrower(String s) throws TestException {
		try {
			if (s.equals("divide")) {
				int i = 0;
				return i/i;
			}
			if (s.equals("null")) {
				s = null;
				return s.length();
			}
			if (s.equals("test")) {
				throw new TestException("Test message");
			}
			return 0;
		} finally {
			System.out.println("[thrower(\"" + s + "\") done]");
		}
	}
}

输入4个参数执行程序:

divide null not test

执行结果:

[thrower("divide") done]
Test "divide" threw a class java.lang.ArithmeticException
with message: / by zero
[thrower("null") done]
Test "null" threw a class java.lang.NullPointerException
with message: null
[thrower("not") done]
Test "not" didn't throw an exception
[thrower("test") done]
Test "test" threw a class TestException
with message: Test message

thrower方法的声明必须有一个throws子句,因为它可以抛出TestException检查异常类(§11.1.1)的实例。如果没有throws子句将会导致一个编译时错误。

需要注意的是finally在每次thrower调用后都会执行,无论有没有异常抛出,就像每次调用后”[thrower(…) done]”输出。