首先看一下异常处理的完整语法,如下:
try{
//(尝试运行的)程序代码
}catch(异常类型 异常的变量名){
//异常处理代码
}finally{
//异常发生,方法返回之前,总是要执行的代码
}
在Java中,应用try-catch-finally结构可以使我们在出现异常的时候能保证相关资源被按时正确的清理。
我们都知道一个try-catch-finally结构,只要try块开始执行了,finally块里面的代码保证执行一次并且只执行一次。try-catch-finally给我们提供了在抛出异常时“保证执行某个动作”的机会,机会是提供了,但是finally一定会把握这次机会,完成某个动作的执行吗?或者说finally能完成它{...}里的相关操作吗?(当然这里除了system.exit()操作,因为在system.exit()时,finally是不会执行的)
下面我们就通过一个例子来说明一下,一个最常见的实例应用就是jdbc的Connection, Statement, ResultSet的使用。
先看一个例子:
例1:
void test(){
Connection conn = ...;
Statement stmt = ...;
ResultSet rset = ...;
rset.close();
stmt.close();
conn.close();
...
}
乍一看,这个程序没有问题,按jdbc标准ResultSet, Statement, Connection都close()掉了,不用GC来帮忙回收资源了。但是如果在rset或者stmt关闭时,出现了异常,那么conn的close()就不能执行了,那么就会存在着浪费资源的问题。也就是说这个例子这样写是有问题的。
这个时候我们就会想到用finally来执行ResultSet, Statement, Connection的close()操作,在finally中,肯定能执行这些close()操作。
例2:
void test(){
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try{
conn = ...;
stmt = ...;
rset = ...;
...
} catch(Exception e){
e.printStackTrace();
} finally{
if(rset!=null)rset.close();
if(stmt!=null)stmt.close();
if(conn!=null)conn.close();
}
}
有人认为这下终于安全了,不会存在资源浪费的情况了。
其实很多人都是这么认为的,但是在finally里close的时候,也是会抛出异常的。如果在执行if(stmt!=null)stmt.close(); 出现了异常,那么conn.close()是不会执行的,finally{}不能完全执行结束。这样写还是有可能出现问题的。
所以就需要对上面的例2再进行改造:
例3:
void test(){
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try{
conn = ...;
stmt = ...;
rset = ...;
...
}
finally{
try{
if(rset!=null)rset.close();
}catch(Exception e){
e.printStackTrace();
}
try{
if(stmt!=null)stmt.close();
}catch(Exception e){
e.printStackTrace();
}
try{
if(conn!=null)conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
这样就可以解决因为异常而存在浪费资源的问题了。一般的Exception通过例3都能解决这些问题。
但是除了Exception,我们的程序还会碰到Error,那么上面这个例子是否也能处理Error呢?众所周知,Error代表不可恢复错误,是程序无法处理的错误,所以我们也处理不了这种Error。所以在finally里执行stmt.close()时,如果出现Error,那么conn.close()还是不会被执行的。finally还是不能执完全行结束。那我们遇到Error时,该怎么处理里?
这里我们可以把Error转成Exception或者RuntimeException,然后抛出,再做处理。因为对于一个应用系统来说,系统所发生的任何异常或者错误对操作用户来说都是系统"运行时"异常,都是这个应用系统内部的异常。这也是将Error转成Exception或者RuntimeException的指导原则(当然也可以将Exception-->RuntimeException)。
①:Error到Exception:将错误转换为异常,并继续抛出。例如Spring WEB框架中,将org.springframework.web.servlet.DispatcherServlet的doDispatch()方法中,将捕获的错误转译为一个NestedServletException异常。这样做的目的是为了最大限度挽回因错误发生带来的负面影响。因为一个Error常常是很严重的错误,可能会引起系统挂起。
②:Exception到RuntimeException:将检查异常转换为RuntimeException可以让程序代码变得更优雅,让开发人员集中经理设计更合理的程序代码,反过来也增加了系统发生异常的可能性。
③:Error到RuntimeException:目的还是一样的。把所有的异常和错误转译为不检查异常,这样可以让代码更为简洁,还有利于对错误和异常信息的统一处理。
所以例3总的来说,还是存在着一些问题,这样对于上面的例3我们又需要进行程序的修改(在例4中就不介绍Error与Exception或者RuntimeException的转换了):
例4:
void test(){
final Connection conn = ...;
try{
final Statement stmt = ...;
try{
final ResultSet rset = ...;
try{
...
}catch(Exception e){
e.printStackTrace();
}finally{rset.close();}
}catch(Exception e){
...
}finally{stmt.close();}
}catch(Exception e){
...
}finally{conn.close();}
}
这样每建立一个需要清理的资源,就用一个try-catch-finally来保证它可以被清理掉。当然了,在执行close操作的时候,我们必须要注意close的次序。通过例4你会发现,close的次序问题好像已经被解决了,不用去考虑这个问题了。
=====================================================================================================
在Java里finally子句的处理还有其他诡异的地方。比如:它可以改写返回值。
如下所示:
public class Test {
public static void main(String[] args) {
System.out.println(ret(-1));
System.out.println(ret(1));
}
private static boolean ret(int i) {
try {
if (i < 0) {
return false;
} else {
return true;
}
}catch (Exception e) {
// TODO: handle exception
}finally{
return false;
}
}
}
两次的返回结果都是:false。其实在调用ret(1)的时候,按道理应该返回true,但是就因为finally里的return操作改写了返回值。所以一般情况下,finally里不会return。
=====================================================================================================
其实在例4中还是存在着一些小问题的:
void test(){
final Connection conn = ...;
try{
final Statement stmt = ...;
try{
final ResultSet rset = ...;
try{④
...③
}catch(Exception e){①
e.printStackTrace();②
}finally{
rset.close();⑤
}
}catch(Exception e){
...
}finally{stmt.close();}
}catch(Exception e){
...
}finally{conn.close();}
}
①: 这里应该指定具体的异常,而不提倡用一个catch(Exception e)语句捕获所有的异常。
catch语句表示我们预期会出现某种异常,而且希望能够处理该异常。异常类的作用就是告诉Java编译器我们想要处理的是哪一种异常。
在这段程序中,最明显的一个就是SQLException,这是JDBC操作中常见的异常。所以我们在捕获这个异常的时候最好能说明具体是那个异常。
如果这try块中有多个异常产生,那么最好是通过多个catch来捕获不同的异常,而不是为了图省事,就用一个catch(Exception e)语句捕获所有的异常。
②:这里异常丢弃了,因为调用一下printStackTrace算不上“处理异常”,调用printStackTrace对调试程序有帮助,但程序调试阶段结束之后,printStackTrace就不应再在异常处理模块中担负主要责任了。
所以在异常处理的时候针对具体异常采取具体行动,例如修正问题、提醒某个人或进行其他一些处理,要根据具体的情形确定应该采取的动作。 认为自己不能处理的一场,可以重新抛出异常也不失为一种选择,或者把该异常转换成另一种异常(可以把一个低级的异常转换成应用级的异常)。
③:如果在这个地方进行数据循环输出,而在循环输出时抛出了异常,那么循环的执行就会被打断的,这样就会导致用户将收到一份不完整的(或者错误的)数据,但却得不到任何有关这份数据是否完整的提示。对于有些系统来说,数据不完整可能比系统停止运行带来更大的损失。
这个时候就要在处理异常的时候向输出设备写一些信息,声明数据的不完整性;另一种可能有效的办法是,先缓冲要输出的数据,准备好全部数据之后再一次性输出,如下所示:
catch(SQLException sqlex)
{
out.println("警告:数据不完整");
throw new SQLException("读取数据时出现SQL错误", sqlex);
}
④:try块不要过于庞大
把大量的语句装入单个巨大的try块就象是把所有东西就扔到一个大箱子,不能是吃的,还是用的,虽然东西是带上了,但要找出来却不容易。
⑤:其实应该跟例3一样
finally{
try{
if(rset!=null)rset.close();
}catch(Exception e){
e.printStackTrace();
}
}