24.异常处理的内在原理及优雅的处理方式

永远不要期待程序在完全理想的状态下运行,异常往往不期而遇,如果没有完善的异常处理机制,后果可能是灾难性的。对于 Java 工程师而言,合理地处理异常是一种基本而重要的能力,然而,在近来的面试中,发现很多应聘者对异常处理的内在原理几无了解,现场手写的异常处理代码也极为“原始”。

鉴于此,为读者呈现 Java 异常处理的内在原理、处理原则及优雅的处理方式。主要内容如下:

  1. Java 异常的层次结构和处理机制
  2. Java 异常表与异常处理的内在原理
  3. .Java 异常处理的基本原则
  4. 优雅地处理 Java 异常案例

1. Java 异常简介

对于 Java 工程师而言,异常应该并不陌生,对 Java 异常的基础知识仅作简要回顾,本文主体将聚焦于深入解读 Java 异常的底层原理和异常处理实践。

1.1 Java 异常类层次结构

在 Java 中,所有的异常都是由 Throwable 继承而来,换言之,Throwable 是所有异常类共同的“祖先”,层次结构图如下所示(注:Error、Exception 的子类及其孙子类只列出了部分):

enter image description here

1.2 Java 异常类相关的基本概念

Throwable

作为所有异常类共同的“祖先”,Throwable 在“下一代”即分化为两个分支:Exception(异常)和 Error(错误),二者是 Java 异常处理的重要子类。

Error

Error 类层次结构用于描述 Java 运行时系统的内部错误和资源耗尽错误,这类错误是程序无法处理的严重问题,一旦出现,除了通告给用户并尽可能安全终止程序外,别无他法。

常见的错误如:

  • JVM 内存资源耗尽时出现的 OutOfMemoryError
  • 栈溢出时出现的 StackOverFlowError
  • 类定义错误 NoClassDefFoundError

这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,它们在应用程序的控制和处理能力之外,一旦发生,Java 虚拟机一般会选择线程终止。

Exception

相较于 Error,Exception 类层次结构所描述的异常更需要 Java 程序设计者关注,因为它是程序本身可以处理的。Exception 类的“下一代”分化为两个分支:RuntimeException + 其它异常

划分两个分支的原则为:

  • 由程序错误导致的异常属于 RuntimeException;
  • 而程序本身没有问题,但由于 I/O 错误之类问题导致的异常属于其它异常。

关于异常和错误的区别:通俗地讲,异常是程序本身可以处理的,而错误则是无法处理的。

可检查异常

可检查异常也称为已检查异常(Checked Exception),这类异常是编译器要求必须处置的异常。在工程实践中,程序难免出现异常,其中一些异常是可以预计和容忍的,比如:

读取文件的时候可能出现文件不存在的情况(FileNotFoundException),但是,并不希望因此就导致程序结束,那怎么办呢?

通常采用捕获异常(try-catch)或者抛出异常(throws 抛出,由调用方处理)的方式来处理。

可检查异常虽然也是异常,但它具备一些重要特征:可预计、可容忍、可检查、可处理。因此,一旦发生这类异常,就必须采取某种方式进行处理。

Java 语言规范将派生于 Error 类或 RuntimeException 类之外的所有异常都归类为可检查异常,Java 编译器会检查它,如果不做处理,无法通过编译。

不可检查异常

与可检查异常相反,不可检查异常(Unchecked Exception)是 Java 编译器不强制要求处置的异常。Java 语言规范将 Error 类和 RuntimeException 及其子类归类为不可检查异常。

为什么编译器不强制要求处置呢?不是因为这类异常简单,危害性小,而是因为这类异常是应该尽力避免出现的,而不是出现后再去补救。以 RuntimeException 类及其子类为例:

  • NullPointerException(空指针异常)
  • IndexOutOfBoundsException(下标越界异常)
  • IllegalArgumentException(非法参数异常)

这些异常通常是由不合理的程序设计和不规范的编码引起的,工程师在设计、编写程序时应尽可能避免这类异常的发生,这是可以做到的。在 IT 圈内有个不成文的原则:如果出现 RuntimeException 及其子类异常,那么可认为是程序员的错误。

1.3 异常处理机制

在 Java 应用程序中,异常处理机制有:抛出异常、捕捉异常。

抛出异常

这里的“抛出异常”是指主动抛出异常。在设计、编写程序时,我们可以预料到一些可能出现的异常,如 FileNotFoundException,有时候我们并不希望在当前方法中对其进行捕获处理,怎么办呢?抛出去,让调用方去处理,通过 throw 关键字即可完成,如:

throw new FileNotFoundException()

关于抛出异常,还有一个点需要补充,那就是声明可检查异常。在设计程序的时候,如果一个方法明确可能发生某些可检查异常,那么,可以在方法的定义中带上这些异常,如此,这个方法的调用方就必须对这些可检查异常进行处理。

声明异常

根据 Java 规范,如果一个 Java 方法要抛出异常,那么需要在这个方法后面用 throws 关键字明确定义可以抛出的异常类型。倘若没有定义,就默认该方法不抛出任何异常。这样的规范决定了 Java 语法必须强行对异常进行 try-catch。如下的方法签名:

public void foo() throws FileNotFoundException { ... } 

暗含了两方面的意思:

  • 第一,该方法要抛出 FileNotFoundException 类型的异常;
  • 第二,除了 FileNotFoundException 外不能(根据规范)抛出其它的异常。

那么,如何保证没有除 FileNotFoundException 之外的任何异常被抛出呢?很显然,方式有:

  • 通过合理的设计和编码避免出现其它异常;
  • 如果其它异常不可完全避免(如方法内调用的其它方法明确可能出现异常),就需要 try-catch 其它的异常。

简而言之,一般情况下,方法不抛出哪些异常就要在方法内部 try-catch 这些异常。

捕获异常

抛出异常十分容易,抛出去便不用再理睬,但是,在一些场景下,必须捕获异常并进行相应的处理。如果某个异常发生后没有在任何地方被捕获,那么,程序将会终止。

在 Java 中,捕获异常涉及三个关键字:try、catch 和 finally。如下举例:

try {
    可能发生异常的代码块
} catch (某种类型的异常 e) {
    对于这种异常的处理代码块
} finally {
    处理未尽事宜的代码块:如资源回收等
}

2. Java 异常表与异常处理的内在原理

在上一部分中,笔者简要介绍了 Java 异常,这里将从字节码的层面切入,剖析 Java 异常处理的内在原理。

2.1 Java 类文件结构简要回顾

众所周知,Java 是一种“与平台无关”的编程语言,其实现“平台无关性”的基石在于虚拟机和字节码的存储格式。事实上,Java 虚拟机并不绑定任何编程语言(包括 Java 语言),而是与“Class 文件”这种特定的二进制格式文件强关联,这种 Class 文件包含了 Java 虚拟机指令集、符号表等信息。

Java 编译器可以将 Java 代码编译成存储字节码的 Class 文件,其它语言,如 JRuby 也可以通过相应的编译器编译为 Class 文件。对于虚拟机而言,并不关心 Class 文件源自何种语言,毕竟,Class 文件才是 Java 虚拟机最终要执行的计算机指令的来源。

Class 文件的格式

Class 文件是一组以 8 位字节为基础单位的二进制流,程序编译后的数据按照严格的顺序紧密排列,其间没有任何分隔符。从数据结构来看,Class 文件采用了一种类似 C 语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数。其中,表主要有方法表、字段表和属性表,为便于读者理解后文的内容,在此着重介绍一下属性表。

属性表(attribute_info)

属性表可以存在于 Class 文件、字段表、方法表中(数据结构是可以嵌套的),用于描述某些场景的专有信息。属性表中有个 Code 属性,该属性在方法表中使用,Java 程序方法体中的代码被编译成的字节码指令存储在 Code 属性中。

异常表(exception_table)

异常表是存储在 Code 属性表中的一个结构,但是,这个结构并不是必须存在的,很好理解,如果方法中根本就没有异常相关的代码,编译结果中自然也不会有异常表。

2.2 异常表解读

异常表结构

异常表的结构如下表所示。它包含 4 个字段,含义为:

如果当字节码在第 start_pc 行到 end_pc 行之间(不包含第 end_pc 行)出现了类型为 catch_type 或者其子类的异常(catch_type 为指向一个 CONSTANT_Class_info 型常量的索引),则跳转到第 handler_pc 行执行。如果 catch_type 的值为 0,则表示任意异常情况都需要转到 handler_pc 处进行处理。

enter image description here

注:u2 是一种数据类型,表示 2 个字节的无符号数。

异常表是 Java 代码的一部分,编译器使用异常表而不是简单的跳转指令来实现 Java 异常及 finally 处理机制。

处理异常的基本原理

根据前面的介绍,不难理解,具备处理异常能力的 Java 类编译后,都会跟随一个异常表,如果发生异常,首先在异常表中查找对应的行(即代码中相应的 try{}catch(){} 代码块),如果找到,则跳转到异常处理代码执行,如果没有找到,则返回(如果有 finally,须在执行 finally 之后),并复制异常给父调用者,接着查询父调用的异常表,以此类推,直至异常被处理或者因没有处理而导致程序终止。

2.3 异常处理实例

为了便于读者更好的理解 Java 异常的处理,在此,结合一个简单的实例来看一下异常表如何运作。 Java 源码如下(本例参考了《深入理解 Java 虚拟机》一书):

public class Test {
    public int inc() { int x; try { x = 1; return x; } catch (Exception e) { x = 2; return x; } finally { x = 3; } } } 

从 Java 语义来看,上述代码的执行路径有以下 3 种:

  • 如果 try 语句块中出现了属于 Exception 及其子类的异常,则跳转到 catch 处理;
  • 如果 try 语句块中出现了不属于 Exception 及其子类的异常,则跳转到 finally 处理;
  • 如果 catch 语句块中出现了任何异常,则跳转到 finally 处理。

由此可以分析上述代码可能的返回结果:

  • 如果没有出现异常,返回 1;
  • 如果出现 Exception 异常,返回 2;
  • 如果出现了 Exception 以外的异常,则非正常退出,没有返回。

将上面的源码编译为 ByteCode 字节码(采用的 JDK 版本为 1.8):

public int inc(); Code: 0: iconst_1 #try中x=1入栈 1: istore_1 #x=1存入第二个int变量 2: iload_1 #将第二个int变量推到栈顶 3: istore_2 #将栈顶元素存入第三个变量,即保存try中的返回值 4: iconst_3 #finally中的x=3入栈 5: istore_1 #栈顶元素放入第二个int变量,即finally中的x=3 6: iload_2 #将第三个int变量推到栈顶,即try中的返回值 7: ireturn #当前方法返回int,即x=1 8: astore_2 #栈顶数值放入当前frame的局部变量数组中第三个 9: iconst_2 #catch中的x=2入栈 10: istore_1 #x=2放入第二个int变量 11: iload_1 #将第二个int变量推到栈顶 12: istore_3 #将栈顶元素存入第四个变量,即保存catch中的返回值 13: iconst_3 #finally中的x=3入栈 14: istore_1 #finally中的x=3放入第一个int变量 15: iload_3 #将第四个int变量推到栈顶,即保存的catch中的返回值 16: ireturn #当前方法返回int,即x=2 17: astore 4 #栈顶数值放入当前frame的局部变量数组中第五个 18: iconst_3 #final中的x=3入栈 19: istore_1 #final中的x=3放入第一个int变量 20: aload 4 #当前frame的局部变量数组中第五个放入栈顶 21: athrow #将栈顶的数值作为异常或错误抛出 Exception table: from to target type 0 4 8 Class java/lang/Exception 0 4 17 any 8 13 17 any 17 19 17 any 

异常表符号解释

从上述字节码中可见,对于 finally 代码块,编译器为每个可能出现的分支后都放置了冗余。并且编译器生成了 3 个异常表记录(在 Exception Table 中),它们分别对应 3 条可能出现的代码执行路径。Exception Table 中包含了很多信息:异常处理开始的偏移量、结束偏移量、异常捕捉的类型等。

  • Exception table:异常处理信息表
  • from:异常处理开始的位置
  • to:异常处理结束的位置
  • target:异常处理器的起始位置,即 catch 开始处理的位置
  • type:异常类型,any 表示所有类型

字节码分析

首先,0~3 行,就是把整数 1 赋值给 x,并且将此时 x 的值复制一个副本到本地变量表的 Slot 中暂存,这个 Slot 里面的值在 ireturn 指令执行前会被重新读到栈顶,作为返回值。这时如果没有异常,则执行 4~5 行,把 x 赋值为 3,然后返回前面保存的 1,方法结束。如果出现异常,读取异常表发现应该执行第 8 行,PC 寄存器指针转向 8 行,8~16 行就是把 2 赋值给 x,然后把 x 暂存起来,再将 x 赋值为 3,然后将暂存的 2 读到操作栈顶返回。第 17~19 行是把 x 赋值为 3,第 20~21 行是将异常放置于栈顶并抛出,方法结束。

3. Java 异常处理的基本原则

在异常处理的整个过程中,需要初始化新的异常对象,从调用栈返回,而且还需要沿着方法的调用链来传播异常以便找到它的异常处理器,因此,相较于普通代码异常处理通常需要消耗更多的时间和资源。为了保证代码的质量,有一些原则需要遵守。

1. 细化异常的类型,避免过度泛化

尽量避免将异常统一写成 Excetpion。原因有二:

  • 针对 try 块中抛出的每种 Exception,很可能需要不同的处理和恢复措施,如果统一为 Excetpion,则只有一个 catch 块,分别处理就不能实现。
  • try 块中有可能抛出 RuntimeException,如果代码中捕获了所有可能抛出的 RuntimeException 而没有作任何处理,则会掩盖编程错误,导致程序难以调试。
try {
    ...
} catch (Exception e) {  // 过分泛华的异常
    ...
}

2.多个异常的处理规则

子类异常的处理块必须在父类异常处理块的前面,否则会发生编译错误。因此,在实践中,越特殊的异常越在前面处理,越普遍的异常越在后面处理。换句话说,能处理就尽早处理,不能处理的就抛出去。当然,对于一个应用系统来说,抛出大量异常是有问题的,应该从程序开发角度尽可能地控制异常发生的可能。

3. 避免过大的 try 块

避免将不会出现异常的代码放到 try 块里面。举个例子:循环的场景,注意 try 代码块的范围。

// 不恰当的方式
try {
    while(rs.hasNext()) {
        foo(rs.next());
    }
} catch (SomeException se) {
    ...
}

// 较为恰当的方式 while(rs.hasNext()) { try { foo(rs.next()); } catch (SomeException se) { ... } } 

4. 延迟捕获

延迟捕获:对异常的捕获和处理需要根据当前代码的能力来决定,如果当前方法内无法对异常做有效处理,即使出现了检查异常也应该考虑将异常抛出给调用者做处理,如果调用者也无法处理,理论上它也应该继续上抛,这样异常最终会在一个适当的位置被 catch 下来,而比起异常出现的位置,异常的捕获和处理是延迟了很多,但同时也避免了不恰当的处理。

5. 对于可检查异常的处理

对于可检查异常,如果不能行之有效地处理,还不如转换为 RuntimeException 抛出。如此,可以让上层的代码有选择的余地。

6.异常处理框架

在实际应用场景中,对于一个应用系统来说,应该要有自己的一套异常处理框架,如此,当异常发生时,就能得到统一的处理风格,将优雅的异常信息反馈给用户。举个例子,如微信、淘宝、支付宝之类的应用,后端涉及的组件非常多,各个组件可能都有自己一套异常处理机制,如果在对接用户(C 端)的口子上不用统一的框架进行处理,那么呈现给用户的异常信息将会失控。

7. 不要忽略异常

对于捕获的异常,可以只打个日志,但是尽量避免什么都不做。

try {
    Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException ex) {} //忽略的异常,挖坑

8. 避免异常转化过程丢失信息

有时候,我们需要将捕获的异常进行转化,但是,在此过程中应尽量避免丢失原始信息,如下反例:

// 抛出异常
try {
    ...
} catch (IOException ioe) {
    throw new Exception(ioe); // 泛化了异常, 外层调用丢失了异常类型的优势 } // 自定义异常 try { ... } catch (SqlException sqle) { throw new MyOwnException(); // 定义了新的异常,但是丢了原始异常信息 } 

9. 生产代码避免 printStackTrace()

// 不好的方式
try {
    ...
} catch (IOException e) {
    e.printStackTrace();
}

try {
    ...
} catch (IOException e) { logger.error("message here" + e); } try { } catch (IOException e) { logger.error("message here" + e.getMessage()); } // 比较好的方式 try { ... } catch (IOException e) { logger.error("message here", e); } 

4. 优雅地处理 Java 异常的案例

通过普通的方式处理 Java 异常并不困难,正因如此,很多工程师忽视了异常的本质和异常处理的原则,在工程实践中长期采用极为“原始”的方式处理异常。在本节中,笔者将基于两个常见的案例讲述如何优雅地处理 Java 异常。

4.1 文件流操作

我们假定需要从一个名为 test.txt 的文件中读取文件流,常见的写法如下:

 public static void main(String[] args) { File file = new File("test.txt"); InputStream inputStream = new FileInputStream(file); inputStream.read(); inputStream.close(); } 

上面这段代码无法通过编译,因为没有做异常处理。我们来看一下常见的异常处理的写法:

public static void main(String[] args) { try { File file = new File("test.txt"); InputStream inputStream = new FileInputStream(file); inputStream.read(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } 

咋眼一看,好像一个 try-catch 就能解决问题了,但有经验的读者很容易看出问题——关闭资源的操作应该写在 finally 块中,改进后如下:

public static void main(String[] args) { try { File file = new File("test.txt"); InputStream inputStream = new FileInputStream(file); inputStream.read(); } catch (IOException e) { e.printStackTrace(); }finally { inputStream.close(); } } 

还是有问题,编译无法通过,因为 inputStream 的作用域有问题,再次改进:

public static void main(String[] args) { InputStream inputStream = null; try { File file = new File("test.txt"); inputStream = new FileInputStream(file); inputStream.read(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } 

为了把 inputStream 对象的作用域带入 finally 块中,我们将 inputStream 的声明放在 try 之外。但这样做,又导致了 inputStream 可能为 null 的情形。所以,一段本来只有两行的代码便演变成这样冗长的代码。

思考时间

通过上述演变的过程,读者应该意识到,导致代码复杂化的关键点所在:我们需要在 finally 这个看起来和 try 平行的代码块中,引用一个变量。

如何优雅地解决问题呢,设想一下如下情形:如果在 new FileInputStream(file) 这一步就出现了问题,那么,inputStream 对象就不需要关闭,因为它根本不存在。这种场景下,代码可以简化如下:

public static void main(String[] args) { File file = new File("test.txt"); try { InputStream inputStream = new FileInputStream(file); inputStream.read(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } 

上面的代码中,通过一个 try-catch 块捕获由 new FileInputStream(file) 引起的异常。如果创建流成功,但读取时发生错误,那么我们必须关闭流以及时释放资源,据此,代码如下:

public List<String> getNames() {
        File file = new File("test.txt"); try { InputStream inputStream = new FileInputStream(file); try { inputStream.read();//核心代码 }finally { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } 

如上所示,通过一个嵌套的 try catch 来处理异常,同时,在 finally 中自然地引用了inputStream 对象。原本冗长的异常处理代码是不是简化了很多?

4.2 JDBC 连数据库

对于绝大多数 Java 工程师而言,通过 JDBC 连数据库的操作应该不会陌生。几个关键步骤:加载驱动、获取连接、执行操作、解析结果集、关闭资源。

为了便于读者理解,在此,通过一段高度模板化的代码来引出问题。如下例子:查找 Student 表中的所有人的名字。

一般的工程师都会写出如下的代码:

public List<String> getNames() {
        ResultSet resultSet = null; PreparedStatement preparedStatement = null; Connection connection = null; try { Class.forName("com.jdbc.mysql.Driver"); connection = DriverManager.getConnection("jdbc:mysql://localhost/test"); preparedStatement = connection.prepareStatement("SELECT names from Student"); resultSet = preparedStatement.executeQuery(); List<String> names = new LinkedList<String>(); while (resultSet.next()) { names .add(resultSet.getString(1)); } return names ; } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } finally { //关闭资源 try { if (resultSet != null) { resultSet.close(); } } catch (SQLException e) { e.printStackTrace(); } try { if (preparedStatement != null) { preparedStatement.close(); } } catch (SQLException e) { e.printStackTrace(); } try { if (connection != null) { connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } } 

为了在 finally 块中能够引用到 resultSet 、preparedStatement 和 connection 三个对象,需要把它们放到了 try 块最外面。然而,这又引发了另一个问题:在到达 finally 时,这些对象可能为 null ,因此又需要加 if 判空,如此,代码就变成了上面那般冗长。

思考时间

回顾一下上述例子,不难发现,其中关键的代码只有四行:它们都有各自的异常抛出。如何优化呢?不妨整理一下代码的主流程,逐层递进,一步一步优化。

 1. Class.forName("com.jdbc.mysql.Driver");
 2. Connection connection =
    DriverManager.getConnection("jdbc:mysql://localhost/test"); 3. PreparedStatement preparedStatement = connection.prepareStatement("SELECT names from Student "); 4. ResultSet resultSet = preparedStatement.executeQuery(); 

场景与优化一

程序需要去加载驱动,不妨设想:如果加载失败了,那肯定没有 connection 以及后面一堆操作什么事儿了,程序应该退出,关闭资源什么的根本不用考虑。鉴于此,只需要加 ClassNotFoundException 声明即可,如下代码:

public List<String> getNames()throws ClassNotFoundException{
            Class.forName("com.jdbc.mysql.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/test"); PreparedStatement preparedStatement = connection.prepareStatement("SELECT names from Student "); ResultSet resultSet = preparedStatement.executeQuery(); List<String> names = new LinkedList<String>(); while (resultSet.next()) { names.add(resultSet.getString(1)); } return names ; } 

场景与优化二

如果驱动加载成功,connection 获取失败。这种情况下,也应该退出程序,也不用关闭资源,因为没有拿到 connection 。鉴于此,只需要加 SQLException 声明即可,代码如下:

public List<String> getNames() throws ClassNotFoundException, SQLException {
        Class.forName("com.jdbc.mysql.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/test"); PreparedStatement preparedStatement = connection.prepareStatement("SELECT name from Student "); ResultSet resultSet = preparedStatement.executeQuery(); List<String> names = new LinkedList<String>(); while (resultSet.next()) { names.add(resultSet.getString(1)); } return names; } 

场景与优化三

如果 connection 获取成功,但构建 preparedStatement 对象失败。这种情况下,需要在退出前关闭 connection,但 preparedStatement 并不需要额外处理,因为根本就没有创建资源。按照预设情况,通过一个 try catch 便可解决,代码如下:

 public List<String> getNames() throws ClassNotFoundException, SQLException {
        Class.forName("com.jdbc.mysql.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/test"); try { PreparedStatement preparedStatement = connection.prepareStatement("SELECT name from Student "); ResultSet resultSet = preparedStatement.executeQuery(); List<String> names = new LinkedList<String>(); while (resultSet.next()) { names.add(resultSet.getString(1)); } return names; } finally { connection.close(); } } 

场景与优化四

如果 preparedStatement 创建成功,但执行失败。这种情况下,connection 和preparedStatement 都需要关闭。 按照假设情况,我们需要再加了一层 try catch,并且在最内层 finally 中关闭已经成功创建的 preparedStatement。代码如下:

public List<String> getNames() throws ClassNotFoundException, SQLException {
        Class.forName("com.jdbc.mysql.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/test"); try { PreparedStatement preparedStatement = connection.prepareStatement("SELECT name from Student "); try { ResultSet resultSet = preparedStatement.executeQuery(); List<String> names = new LinkedList<String>(); while (resultSet.next()) { names.add(resultSet.getString(1)); } return names; } finally { preparedStatement.close(); } } finally { connection.close(); } } 

场景与优化五

如果 resultSet 创建成功,但在遍历中出现问题,或者整个过程没有问题,需要关闭所有资源,退出程序。进一步处理,得到如下代码:

public List<String> getNames() throws ClassNotFoundException, SQLException {
        Class.forName("com.jdbc.mysql.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/test"); try { PreparedStatement preparedStatement = connection.prepareStatement("SELECT name from Student "); try { ResultSet resultSet = preparedStatement.executeQuery(); try { List<String> names = new LinkedList<String>(); while (resultSet.next()) { names.add(resultSet.getString(1)); } return names; } finally { resultSet.close(); } } finally { preparedStatement.close(); } } finally { connection.close(); } } 

案例小结

如上所示,代码最终的演化结果,相较于普通的处理方式简化了很多,与此同时,不会漏关闭任何一个资源,也不必写一堆难看的判断空的代码。

猜你喜欢

转载自www.cnblogs.com/Pibaosi/p/10677403.html
今日推荐