异常
目标
- 学习异常处理基础知识
- 了解异常分层结构和如何使用多个 catch 代码块
异常处理基础
没有程序能够始终正常运行,Java 语言的设计者也知道这一点。Java 平台提供了内置机制来处理代码未准确地按计划运行的情形。
异常 是在程序执行期间发生的破坏正常的程序指令流的事件。异常处理是 Java 编程的一项基础技术。您将代码包装在一个 try 代码块中(这表示 “尝试此代码并让我知道它是否导致了异常”),并使用它捕获 (catch) 各种类型的异常。
要开始执行异常处理,可以看看清单 1 中的代码。
清单 1. 您发现错误了吗?
@Test
public void yetAnotherTest() {
Logger l = Logger.getLogger(Employee.class.getName());
// Employee employee1 = new Employee();
Employee employee1 = null;
employee1.setName("J Smith");
Employee employee2 = new Employee();
employee2.setName("J Smith");
l.info("Q: employee1 == employee2? A: " + (employee1 == employee2));
l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
}
请注意,Employee 引用被设置为 null。运行此代码,您会获得以下输出:
java.lang.NullPointerException
at com.makotojava.intro.EmployeeTest.yetAnotherTest(EmployeeTest.java:49)
.
.
.
此输出会告诉您,您在尝试通过一个 null 引用(指针)来引用一个对象,这是一个非常严重的开发错误。(您可能已注意到,Eclipse 通过下面这条消息向您提醒潜在的错误:Null pointer access:The variable employee1 can only be null at this location.Eclipse 向您提醒许多潜在的开发错误,这也是使用 IDE 执行 Java 开发的另一个优势。)
幸运的是,可以使用 try 和 catch 代码块(以及来自 finally 的一些帮助)捕获错误。
使用 try、catch 和 finally
清单 2 显示了使用标准的异常处理代码块从 清单 1 中清除的错误代码:try、catch 和 finally。
清单 2. 捕获一个异常
@Test
public void yetAnotherTest() {
Logger l = Logger.getLogger(Employee.class.getName());
// Employee employee1 = new Employee();
try {
Employee employee1 = null;
employee1.setName("J Smith");
Employee employee2 = new Employee();
employee2.setName("J Smith");
l.info("Q: employee1 == employee2? A: " + (employee1 == employee2));
l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
} catch (Exception e) {
l.severe("Caught exception: " + e.getMessage());
} finally {
// Always executes
}
}
try、catch 和 finally 代码块共同形成了一张捕获异常的网。首先,try 语句限定可能抛出异常的代码。在该例子中,异常直接放在 catch 代码块或异常处理函数 中。在所有尝试和捕获都完成后,会继续执行 finally 代码块,无论是否发生了异常。捕获到异常时,可以尝试优雅地进行恢复,也可以退出程序(或方法)。
在 清单 2 中,程序从错误中恢复,然后打印异常的消息:
Sep 19, 2015 2:01:22 PM com.makotojava.intro.EmployeeTest yetAnotherTest
SEVERE: Caught exception: null
异常分层结构
Java 语言包含一个完整的异常分层结构,它由许多类型的异常组成,这些异常划分为两大主要类别:
已检查的异常已由编译器检查(表示编译器确定它们已在您代码中的某处处理过)。一般而言,这些异常是 java.lang.Exception 的直接子类。 未检查的异常(也称为运行时异常)未由编译器检查。这些是 java.lang.RuntimeException 的子类。 程序导致异常时,您可以说它抛出了 异常。已检查的异常可由任何方法在方法名中包含 throws 关键字来声明。后跟该方法可能在执行期间抛出的异常的逗号分隔列表。如果代码调用的一个方法指定它抛出一种或多种类型的异常,您必须对它进行一定的处理,或者向方法名中添加一个 throws 来传递该异常类型。
发生异常时,Java 运行时在堆栈中往上搜索一个异常处理函数。如果在到达堆栈顶部时仍未找到异常处理函数,它会立即终止程序,就像在 清单 1 中看到的那样。
多个 catch 代码块
可以拥有多个 catch代码块,但必须采用某种特定方式来搭建它们。如果所有异常都是其他异常的子类,那么子类会按照 catch 代码块的顺序放在父类前面。清单 3 给出了一个按正确的分层结构顺序搭建的不同异常类型的示例。
清单 3. 异常分层结构示例
@Test
public void exceptionTest() {
Logger l = Logger.getLogger(Employee.class.getName());
File file = new File("file.txt");
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new FileReader(file));
String line = bufferedReader.readLine();
while (line != null) {
// Read the file
}
} catch (FileNotFoundException e) {
l.severe(e.getMessage());
} catch (IOException e) {
l.severe(e.getMessage());
} catch (Exception e) {
l.severe(e.getMessage());
} finally {
// Close the reader
}
}
在这个示例中,FileNotFoundException 是 IOException 的子类,所以它必须放在 IOException catch 代码块的前面。IOException 是 Exception 的子类,所以它必须放在 Exception catch 代码块的前面。
try-with-resources 代码块
清单 3 中的代码必须声明一个变量来包含 bufferedReader 引用,然后在 finally 中必须关闭 BufferedReader。
在 try 代码块超出范围时,更加紧凑的语法(从 JDK7 开始提供)也可以自动关闭资源。清单 4 给出了这种更新的语法。
清单 4. 资源管理语法
@Test
public void exceptionTestTryWithResources() {
Logger l = Logger.getLogger(Employee.class.getName());
File file = new File("file.txt");
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
String line = bufferedReader.readLine();
while (line != null) {
// Read the file
}
} catch (Exception e) {
l.severe(e.getMessage());
}
}
基本上讲,在圆括号内的 try 后分配资源变量,在 try 代码块超出范围时,这些资源会自动关闭。这些资源必须实现 java.lang.AutoCloseable 接口;如果尝试在一个没有实现该接口的资源类上实现此语法,Eclipse 会提醒您。