永远不要期待程序在完全理想的状态下运行,异常往往不期而遇,如果没有完善的异常处理机制,后果可能是灾难性的。对于 Java 工程师而言,合理地处理异常是一种基本而重要的能力,然而,在近来的面试中,笔者发现很多应聘者对异常处理的内在原理几无了解,现场手写的异常处理代码也极为“原始”。
鉴于此,笔者将通过本场 Chat 为读者呈现 Java 异常处理的内在原理、处理原则及优雅的处理方式。主要内容如下:
- Java 异常的层次结构和处理机制
- Java 异常表与异常处理的内在原理
- .Java 异常处理的基本原则
- 优雅地处理 Java 异常案例
- Java 异常简介
对于 Java 工程师而言,异常应该并不陌生,作为本场 Chat 的引入部分,对 Java 异常的基础知识仅作简要回顾,本文主体将聚焦于深入解读 Java 异常的底层原理和异常处理实践。
1.1 Java 异常类层次结构
在 Java 中,所有的异常都是由 Throwable 继承而来,换言之,Throwable 是所有异常类共同的“祖先”,层次结构图如下所示(注:Error、Exception 的子类及其孙子类只列出了部分):
1.2 Java 异常类相关的基本概念
Throwable
作为所有异常类共同的“祖先”,Throwable 在“下一代”即分化为两个分支:Exception(异常)和 Error(错误),二者是 Java 异常处理的重要子类。
Error
Error 类层次结构用于描述 Java 运行时系统的内部错误和资源耗尽错误,这类错误是程序无法处理的严重问题,一旦出现,除了通告给用户并尽可能安全终止程序外,别无他法。
常见的错误如:
JVM 内存资源耗尽时出现的 OutOfMemoryError
栈溢出时出现的 StackOverFlowError
类定义错误 NoClassDefFoundError
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,它们在应用程序的控制和处理能力之外,一旦发生,Java 虚拟机一般会选择线程终止。对于新手小白想更轻松的学好Java提升,Java架构,web开发、大数据,数据分析,人工智能等技术,这里给大家分享系统教学资源,扩列下我尉(同英):CGMX9880 【教程/工具/方法/解疑】
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 及其子类异常,那么可认为是程序员的错误。对于新手小白想更轻松的学好Java提升,Java架构,web开发、大数据,数据分析,人工智能等技术,这里给大家分享系统教学资源,扩列下我尉(同英):CGMX9880 【教程/工具/方法/解疑】
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 { 处理未尽事宜的代码块:如资源回收等 }
- 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 处进行处理。
注: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 行是将异常放置于栈顶并抛出,方法结束。
- Java 异常处理的基本原则
在异常处理的整个过程中,需要初始化新的异常对象,从调用栈返回,而且还需要沿着方法的调用链来传播异常以便找到它的异常处理器,因此,相较于普通代码异常处理通常需要消耗更多的时间和资源。为了保证代码的质量,有一些原则需要遵守。
- 细化异常的类型,避免过度泛化
尽量避免将异常统一写成 Excetpion。原因有二:
针对 try 块中抛出的每种 Exception,很可能需要不同的处理和恢复措施,如果统一为 Excetpion,则只有一个 catch 块,分别处理就不能实现。
try 块中有可能抛出 RuntimeException,如果代码中捕获了所有可能抛出的 RuntimeException 而没有作任何处理,则会掩盖编程错误,导致程序难以调试。
try { … } catch (Exception e) { // 过分泛华的异常 … }
2.多个异常的处理规则
子类异常的处理块必须在父类异常处理块的前面,否则会发生编译错误。因此,在实践中,越特殊的异常越在前面处理,越普遍的异常越在后面处理。换句话说,能处理就尽早处理,不能处理的就抛出去。当然,对于一个应用系统来说,抛出大量异常是有问题的,应该从程序开发角度尽可能地控制异常发生的可能。
- 避免过大的 try 块
避免将不会出现异常的代码放到 try 块里面。举个例子:循环的场景,注意 try 代码块的范围。
// 不恰当的方式try { while(rs.hasNext()) { foo(rs.next()); } } catch (SomeException se) { … }// 较为恰当的方式while(rs.hasNext()) { try { foo(rs.next()); } catch (SomeException se) { … } }
- 延迟捕获
延迟捕获:对异常的捕获和处理需要根据当前代码的能力来决定,如果当前方法内无法对异常做有效处理,即使出现了检查异常也应该考虑将异常抛出给调用者做处理,如果调用者也无法处理,理论上它也应该继续上抛,这样异常最终会在一个适当的位置被 catch 下来,而比起异常出现的位置,异常的捕获和处理是延迟了很多,但同时也避免了不恰当的处理。
- 对于可检查异常的处理
对于可检查异常,如果不能行之有效地处理,还不如转换为 RuntimeException 抛出。如此,可以让上层的代码有选择的余地。
6.异常处理框架
在实际应用场景中,对于一个应用系统来说,应该要有自己的一套异常处理框架,如此,当异常发生时,就能得到统一的处理风格,将优雅的异常信息反馈给用户。举个例子,如微信、淘宝、支付宝之类的应用,后端涉及的组件非常多,各个组件可能都有自己一套异常处理机制,如果在对接用户(C 端)的口子上不用统一的框架进行处理,那么呈现给用户的异常信息将会失控。
- 不要忽略异常
对于捕获的异常,可以只打个日志,但是尽量避免什么都不做。
try { Class.forName(“com.mysql.jdbc.Driver”); } catch (ClassNotFoundException ex) {} //忽略的异常,挖坑
- 避免异常转化过程丢失信息
有时候,我们需要将捕获的异常进行转化,但是,在此过程中应尽量避免丢失原始信息,如下反例:
// 抛出异常try { … } catch (IOException ioe) { throw new Exception(ioe); // 泛化了异常, 外层调用丢失了异常类型的优势}// 自定义异常try { … } catch (SqlException sqle) { throw new MyOwnException(); // 定义了新的异常,但是丢了原始异常信息}
- 生产代码避免 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); }
- 优雅地处理 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 对象就不需要关闭,因为它根本不存在。这种场景下,代码可以简化如下:
上面的代码中,通过一个 try-catch 块捕获由 new FileInputStream(file) 引起的异常。如果创建流成功,但读取时发生错误,那么我们必须关闭流以及时释放资源,据此,代码如下:
public List 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 连数据库的操作应该不会陌生。几个关键步骤:加载驱动、获取连接、执行操作、解析结果集、关闭资源。对于新手小白想更轻松的学好Java提升,Java架构,web开发、大数据,数据分析,人工智能等技术,这里给大家分享系统教学资源,扩列下我尉(同英):CGMX9880 【教程/工具/方法/解疑】
为了便于读者理解,在此,通过一段高度模板化的代码来引出问题。如下例子:查找 Student 表中的所有人的名字。
一般的工程师都会写出如下的代码:
public List 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 names = new LinkedList(); 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 判空,如此,代码就变成了上面那般冗长。
思考时间
回顾一下上述例子,不难发现,其中关键的代码只有四行:它们都有各自的异常抛出。如何优化呢?不妨整理一下代码的主流程,逐层递进,一步一步优化。
- 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 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 names = new LinkedList();
while (resultSet.next())
{
names.add(resultSet.getString(1));
}
return names ;
}
场景与优化二
如果驱动加载成功,connection 获取失败。这种情况下,也应该退出程序,也不用关闭资源,因为没有拿到 connection 。鉴于此,只需要加 SQLException 声明即可,代码如下:
public List 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 names = new LinkedList();
while (resultSet.next())
{
names.add(resultSet.getString(1));
}
return names;
}
场景与优化三
如果 connection 获取成功,但构建 preparedStatement 对象失败。这种情况下,需要在退出前关闭 connection,但 preparedStatement 并不需要额外处理,因为根本就没有创建资源。按照预设情况,通过一个 try catch 便可解决,代码如下:
public List 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 names = new LinkedList();
while (resultSet.next())
{
names.add(resultSet.getString(1));
}
return names; } finally
{
connection.close();
}
}
场景与优化四
如果 preparedStatement 创建成功,但执行失败。这种情况下,connection 和preparedStatement 都需要关闭。 按照假设情况,我们需要再加了一层 try catch,并且在最内层 finally 中关闭已经成功创建的 preparedStatement。代码如下:
public List 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 names = new LinkedList();
while (resultSet.next())
{
names.add(resultSet.getString(1));
}
return names;
}
finally { preparedStatement.close();
}
} finally { connection.close();
}
}
场景与优化五
如果 resultSet 创建成功,但在遍历中出现问题,或者整个过程没有问题,需要关闭所有资源,退出程序。进一步处理,得到如下代码:
public List 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 names = new LinkedList();
while (resultSet.next())
{
names.add(resultSet.getString(1));
}
return names;
}
finally
{
resultSet.close();
}
}
finally
{ preparedStatement.close();
}
} finally
{
connection.close();
}
}
案例小结
如上所示,代码最终的演化结果,相较于普通的处理方式简化了很多,与此同时,不会漏关闭任何一个资源,也不必写一堆难看的判断空的代码。
欢迎在下发评论区与我们交流~