1. 异常¶
1.1. 学习目标¶
完成本模块后,学生将能够:
回顾异常和异常处理的作用
回顾并使用 try-catch-finally 块来处理异常
回顾如何在方法中抛出异常
回顾如何实现和测试异常处理代码
1.1.1. 建议阅读¶
来自 Data Structures and Abstractions with Java, 4th edition by Frank M. Carrano and Timothy Henry 的 Java 插曲 2:异常
1.2. 交互式:异常处理¶
1.3. 反思受检异常与运行时(未受检)异常¶
1.3.1. 受检异常(Checked Exceptions)¶
这些是在编译时被"检查"的异常
受检异常 必须 要么
被捕获(使用 try-catch),要么
在抛出该异常的方法中声明(使用 throws)——见下面的示例
public void myMethod() throws IOException { // programming statements }
如果受检异常没有被处理或被抛出,编译器在尝试编译该程序时会报告编译错误
受检异常的例子包括:ClassNotFoundException、
FileNotFoundException、IOException 和 NoSuchMethodException
1.3.2. 运行时异常又称未受检异常¶
这些是在编译时不被"检查"的异常
与受检异常不同,任何可能抛出运行时(未受检)异常的代码都能毫无问题地编译,因此开发人员必须考虑此类异常发生的可能性,并相应地准备程序
运行时(未受检)异常的例子包括:
ArithmeticException、ArrayIndexOutOfBoundsException、ClassCastException和NullPointerException
注意:
很多学生在各种项目和实验中都会遇到 NullPointerExceptions。始终记住,当你的代码试图访问一个为 null 的对象时,就会发生这种异常。同时也要记住,如果对象被声明但从未实例化,或者在其生命周期中的某个时刻变成 null,那么对象就可能是 null。排查此类异常的一种简便方法是确定该对象当时是否确实被实例化了。只需检查你认为对象被实例化的那部分代码,然后评估和测试,以确认那部分代码确实按预期执行了。
1.4. 检查点 1¶
1.5. 交互式:使用 try、catch 和 finally 进行异常处理¶
要在代码主体中实现异常处理,必须使用 try-catch-finally 块
你可以使用单个 catch 块,例如下面这样:
try {
}
catch(ExceptionType eName) {
}
也可以使用多个 catch 块。当使用多个 catch 块时,异常处理程序的实现顺序很重要。代码应该把最特定异常的处理器放在最前面,逐步过渡到最一般异常的定义。
例如:
try{
}
catch(NumberFormatException nfe){
}
catch(Exception e){
}
我们也可以使用 可选 的 finally 块。例如:
try{
}
catch(Exception e){
}
finally{
}
注意,你也可以将可选的 finally 块与 try 一起使用,但不使用 catch ,例如下面这样:
try{
}
finally{
}
1.6. 使用 throws 稍后处理异常¶
如上所述,有时适合在引起异常的代码主体内部实现捕获这些异常的代码,本质上是在可能引起异常的代码主体内处理异常。而其他时候,推迟处理给定的异常可能更合适,即把处理它的责任转交给或传递给某个其他调用代码(客户端代码)。当存在这样的预期或约定,即调用代码(客户端代码)可能更适合处理该异常时,这个选项就是合适的。回想起正是客户端代码调用了引起异常的方法,因此有理由认为这样的客户端代码可能更适合决定响应该异常事件的最佳方式。
要推迟或延后把异常处理转交给客户端代码,我们就在潜在风险方法(可能引起异常的方法)的方法声明中加入 throws 子句。 throws 子句必须跟在方法名和括号的后面,在这个子句中,我们必须使用 throws 关键字,并提供该方法抛出的所有异常的逗号分隔列表(见下面的示例)。
public void riskyCodeThatWantsToDefer ( ) throws IOException, NumberFormatException {
// some code
}
throws 子句声明了程序执行期间可能发生的异常,本质上是在告知调用/客户端代码,它需要做好准备来处理此类情况, 要么自己实现 try-catch,要么通过在方法声明中添加 throws 子句,把处理转交给其他调用/客户端代码。
例如,下面的方法调用了这段有风险的代码,因此它会被告知应该预期 IOException、NumberFormatException 会找上门来,并做好相应准备。
public void callingMethod() {
riskyCodeThatWantsToDefer();
}
如果客户端代码,即 callingMethod,没有包含捕获或抛出 riskyCodeThatWantsToDefer 所声明会抛出的异常的代码,那么就会出现编译错误消息(Unhandled exception type <SomeExceptionName>)。
1.7. 异常处理示例——从基础到复杂¶
自己试一试
在 Eclipse 中自行下载并运行、探索视频中的对应项目。示例项目需要 CS2-Support 项目。它也会用在你平时的课程项目中。要下载 CS2-Support,你必须先完成第一次实验的配置步骤。然后就可以通过 Eclipse 使用蓝色向下箭头图标,或使用项目菜单并选择"Download Assignment..."来下载它。
1.7.1. 交互式:基础 try、catch 示例¶
1.7.2. 交互式:追踪使用多个 catch 块的 try、catch 示例¶
1.7.3. 交互式:使用 try、catch 和 finally 块¶
1.8. 实现与测试异常¶
"如果你在方法中抛出异常,那么就应该在测试中捕获它们"
在本课程中,我们将 主要 采用把 try-catch 块 与 throw 语句相结合的异常处理方法。
在实现含有容易引发异常的代码的方法时,你需要在方法内实现这样的代码:在允许执行有风险的代码(可能抛出异常的代码) 之前,先检查异常情况(可能的异常事件)。然后必须在测试类中实现 try-catch blocks,以确认出错的代码确实抛出了正确的异常。在测试类中,你还必须为抛出异常创造必要的条件。
注意,这些检查可以通过使用 try-catch 块或条件语句(例如 if 语句)来完成。
你的代码应按照如下方式运行:
如果检查通过,则代码正常继续执行,让容易引发异常的代码得以运行
如果检查失败,则方法应 故意抛出一个异常
1.8.1. 抛出异常¶
任何代码在适当的条件下都有能力抛出异常。你可以用 throw 语句故意抛出一个异常。你可以抛出已存在的众多异常中的任何一个,更具体地说,可以是 Throwable 类的任何后代类。
如果有必要,你也可以创建自定义异常类,以应对标准 Java 异常类尚未涵盖的意外情况。
要抛出异常,你必须为 throw 语句提供一个 可抛出对象(throwable) 的实例。
例如,如果你只想抛出一个普通的异常,可以使用下面的语句:
throw new Exception();
另一方面,如果你想抛出一个特定的异常,比如 NumberFormatException,你可以使用下面的语句:
throw new NumberFormatException();
// or
throw new NumberFormatException( "this is some message" );
所需的一切只是了解你想要抛出的异常有哪些可用的构造器。
1.8.1.1. 示例¶
下面的 MyCalculator 类为客户端代码提供了访问 sum() 和 div() 方法的途径。sum() 和 div() 各自接受两个表示整数的 String 参数。sum() 方法返回两个参数的 int 等价形式相加的结果,而 div() 方法返回两个参数的 int 等价形式相除的结果。
public class MyCalculator {
public int sum(String num1String, String num2String) {
int sum = 0;
try {
int num1 = Integer.parseInt(num1String);
int num2 = Integer.parseInt(num2String);
sum = num1 + num2;
}
catch (NumberFormatException nfe) {
throw new NumberFormatException();
}
return sum;
}
public int div(String num1String, String num2String) {
int div = 0;
try {
int num1 = Integer.parseInt(num1String);
int num2 = Integer.parseInt(num2String);
div = num1 / num2;
}
catch (NumberFormatException nfe) {
throw new NumberFormatException();
}
catch (ArithmeticException ae) {
throw new ArithmeticException();
}
return div;
}
}
使用上面的代码示例来回顾 MyCalculator 类。注意该类是如何使用这些语句的:
throw new NumberFormatException();
和
throw new ArithmeticException();
来在适当的时候有意抛出每种异常。
因此,在编写测试类时,你必须使用 try-catch 块来检查你的方法代码是否抛出了正确的异常。在 try 块中,你应该调用那个会导致抛出异常的方法。catch 块应该捕获抛出的异常。然后你必须断言该异常存在、是正确的异常,并且(如果适用)包含正确的消息。
观察部分实现的测试类 MyCalculatorTest。
这个类将用于评估 MyCalculator 类,以确定该类是否为每个测试用例抛出了正确的异常。
注意到测试类采用了上面描述的做法:声明一个与被测异常匹配的异常对象。最初这个异常对象被设置为 null,只在 catch 块内更新。
public class MyCalculatorTest extends student.TestCase {
MyCalculator calc;
public void setUp() {
calc = new MyCalculator();
}
/**
* Tests to ensure Sum throws a NumberFormatException
* if the first parameter is not a number
*/
public void testSumNFEException() {
NumberFormatException myNFE = null;
try {
calc.sum("2hello", "3");
}
catch (NumberFormatException nfe) {
myNFE = nfe;
}
assertNotNull(myNFE);
}
/**
* Tests to determine if div throws an ArithmeticException
* if one of the parameters is 0
*/
public void testDivArithException() {
ArithmeticException myAE = null;
try {
calc.div("2", "0");
}
catch (ArithmeticException ae) {
myAE = ae;
}
assertNotNull(myAE);
}
}
