这是几年前我写的一篇文档,拿出来分享。可能有些内容过时和不够用了。
引言
本文对Java异常处理机制以及常用的异常处理方式进行了大致的描述。鉴于各种有关Java的文档中对于异常处理有很多的说明,因此,本文主要侧重于说明怎样在面向对象分析和设计(OOA & OOD)中进行异常设计,以及与我们常用的异常处理方式相关但容易出现问题的地方进行了探讨。目前,关于异常设计的规范还没有很好的标准,本文抛砖引玉,希望能从大家对异常处理的见解和经验中,寻求一种好的方式来规范我们的异常设计。
1 异常处理的重要性
异常处理是Java语言本身的一项重要特性。对于创建创建大型、健壮、易于维护的程序来讲是非常重要的。在《Thinking in Java》中,对异常的应用指出了以下几点:
- 解决问题并再次调用造成违例的方法。
- 平息事态的发展,并在不重新尝试方法的前提下继续。
- 计算另一些结果,而不是希望方法产生的结果。
- 在当前环境中尽可能解决问题,以及将相同的违例重新“掷”出一个更高级的环境。
- 在当前环境中尽可能解决问题,以及将不同的违例重新“掷”出一个更高级的环境。
- 中止程序执行。
- 简化编码。若违例方案使事情变得更加复杂,那就会令人非常烦恼,不如不用。使自己的库和程序变得更加安全。这既是一种“短期投资”(便于调试),也是一种“长期投资”(改善应用程序的健壮性)
1.1 在编译期间对错误进行捕获
与C语言不同的是,Java在编译时就要求对所有可能出现的可检查异常进行捕获(关于可检查异常的解释见2.1),从而保证了在运行时程序对于这种意外情况进行了处理,从而保证了程序的健壮性。
1.2 简化错误控制代码
和Java相对比,C语言对于错误的控制是通过返回一个值或设置一个标志(位),接收者会检查这些值或标志,判断具体发生了什么事情,这种方法导致正常流程和错误流程混杂在一起,使程序庞大,造成阅读和理解时的困难。而异常控制机制则可以将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。一般情况下,用于读取、写入以及调试的代码会变得更富有条理。同时也有效减少了代码量。
2 异常的分类与常用的异常处理方法
我们在这里所说的异常,包括了狭义的异常(Exception)和错误(Error)两种,也就是java.lang.Throwable类,java.lang. Exception和java.lang.Error是它的子类。
狭义的异常是指java.lang.Exception及其子类,其中又分为两种,第一种,编译器强制违例规范捕获它们,就要求我们在编译之前对这样的异常做好应对的处理,例如java.io.IOException、java.sql.SQLException等等;第二种,指java.lang.RuntimeException及其子类,对于这一类异常,编译器不要求为这些类型指定违例规范,例如java.lang.NullPointerException。
2.1 可检查异常
对于那些编译器强制要求进行捕获的异常,我们称为可检查异常。假如程序中,对这些语句没有使用异常控制的话,会发生编译错误。
2.2 运行时异常
运行时异常Java语言并不强制我们在程序中进行捕获,例如ClassCastException、EmptyStackException、 IllegalArgumentException、IndexOutOfBoundsException、SecurityException、SystemException、 UnsupportedOperationException等等。
运行时异常不要求捕获的原因是在程序中,很多的操作都有可能会导致运行时异常,编译器根据已知的信息,并不能确信这种异常不会发生,但是对于程序员来讲,又是显然不会发生异常的。如果对这样的异常也需要显式加以捕
获或者通过throws语句来抛出,显然会使程序很混乱。
2.3 错误
错误(java.lang.Error)是指比较严重的问题,大多数错误是因为不正常的条件所引起的。因为它可能会在程序的很多地方出现,而且很难从错误中恢复,如果对这样的错误也要在编译时强制要求捕获的话,会使程序显得非常混乱。
例如线程死锁,就是一个Error类的子类,因为大多数程序并不能对之捕获和处理。
例如java.lan4g.VerifyError,当Java虚拟机对字节码校验出现错误时,抛出这样的错误。
对于程序在运行时所遇到的错误,可以采用try...catch语句,进行处理(一般这种错误都不是由程序处理,而且这种错误出现的很少),对不能匹配到相应catch语句的错误则直接抛出。
2.4 处理方法
2.4.1 异常捕获
我们一般采用下面的方法来处理异常和错误:
try{
//正常处理流程
}catch( Throwable ex){
//异常处理
}finally{
//释放例如输入输出流、数据库连接之类的
//不会被GC自动回收的资源
}
其中,catch()语句所catch到的为 Throwable对象,包括了所有的Exception和Error。
2.4.2 获取异常处理信息
这是最最普遍的一种处理方式,如下:
try{...}
catch( Exception ex ){
//ex.getMessage()就包含了异常的说明信息
}
这样做的好处是我们可以对这个Exception的信息有比较灵活自由的处理方式,比如打印到标准输出流,记录到日志等等,缺点就是当出现NullPointerException时,异常的getMessage()返回为null,对之进行处理会抛出一个我们没有进行捕获的NullPointerException。适用于给用户显式异常信息。
2.4.3 打印调用堆栈
在程序的调试过程中,我们一般采用这种处理,如下:
try{...}
catch( Exception ex ){
ex.printStackTrace();
}
这样做的好处就是可以从调用堆栈里找出引发异常的根本原因,缺点是信息不够友好。适合用来调试程序。
2.4.4 抛出异常
这种方法的用法如下:
在方法的内部:
try{...}
catch( Exception ex ) throw ex; .....方式1
或者在方法说明时,就声明会抛出的异常,例如:
public void doSomeThing() throws Exception .....方式2
这种方法的优点就是将异常一直传递到一个适合处理的层级,从而由适合处理这个异常的角色来觉得应该以何种方式来对异常作相应的处理。缺点就是如果没有经过良好的设计,容易“迷失”引发异常的真正原因。这种方式适合在API类型的产品中采用。
在实际使用中,方式1和方式2常常结合使用,例如下面的方法:
public void method1() throws MyException{
try{
}catch( SomeException ex ){
doSomething();
throw new MyException( ex.getMessage() );
}fiinally{
doMyFinally(); //必要的清楚工作
}
2.4.5 其它处理
在JSP中,我们常常采用errorPage机制,如果在当前页面捕获到错误,则自动转到出错页面,出错页面的代码,一般的写法如下:
<%@ page isErrorPage="true" %>
<%
String message = exception.getMessage();
out.println( message );
%>
在JSP中,可以采用如下的方式来引用
<%@ page errorPage="error.jsp"%>
这个语句也就指示Web服务器在执行JSP页面代码时如果出现异常,则转到指定的页进行异常处理,包括打印出异常信息等等。如果没有指定错误页,则在出现异常时有的WEB服务器会直接抛出错误。
3 当前异常处理中的薄弱环节
3.1 系统设计中没有异常设计
目前,采用UML进行系统的分析和设计已经成为Java设计和开发中的重要环节,UML中的类图、用例图、顺序图等等都对Java开发起到了很好的辅助作用。但是据笔者的经验,大多数的UML设计都是针对一种相对理想化的情况来进行设计的,这样的确也起到了简化设计的效果,但是这样就把对系统中异常的处理完全交给了实现阶段,造成的第一个后果就是灵活性过高,每一块程序的编写者都可以按照自己的好恶和习惯来定义和处理所涉及的异常。造成代码中有关异常部分的管理混乱。我们的设计的出发点和评判标准应该是设计是否对开发过程(主要是编码)是不是能起到指导作用,而不是它是否符合标准。
然而事实上对错误的预防和对于运行期错误的处理,是一个软件中很重要的一部分,对于一个健壮的系统,据统计,用于错误处理的代码量,要大大超过理想化情况下实现功能的代码。所以,在设计时,没有对异常的设计(以Rational Rose、Together或者说UML来说,没有有关异常的设计,在顺序图中也对此只字不提,笔者以为,是很荒谬的),所造成的结果就是实现时,只有少数的、按照理想化的环境和操作实现软件功能的代码部分是可控制的,而在代码中占了很大比重的异常处理部分,则没有整体设计,完全由实现者临时来决定。
因此,笔者建议在我们进行系统OO设计的阶段,就对对系统中可能出现的异常进行设计,这个设计主要包括以下的内容:
- 自定义异常类的定义,包括命名、包装和层次
- 异常信息的组织
- 异常抛出的时机
- 对异常的处理
对于这些内容,我们将在第4部分进行详细分析。
3.2 异常处理流于简单的形式
目前的程序中,我们常常采用的异常处理都比较简单,包括调试期间的
ex.printStackTrace();
和在JSP中的errorPage,以及API内部的throw,从上边对几种方法的分析,我们可以知道,在决定一个异常是应该抛出还是进行处理(如何处理)的时候,必须参考特定程序的上下文环境,包括程序的性质、程序的使用者,以及引发该异常的原因、异常是否可以恢复等等具体的原因。
由此也可以看到,如果我们在实现一个系统之前的设计环节没有对异常进行很好的设计,而是由实现者按照自己的实现方式来做,那么异常部分就很容易失去控制。
在异常处理做的不好的程序中,还可以发现这样的程序:
try{
//...
}catch( Exception ex ){
//...
}
这样的程序,应该尽量避免,我们可以参照Java源代码中的一段来看:
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
this();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
try {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkListen(port);
}
impl.create(true); // a stream socket
if (bindAddr == null)
bindAddr = InetAddress.anyLocalAddress;
impl.bind(bindAddr, port);
impl.listen(backlog);
} catch(SecurityException e) {
impl.close();
throw e;
} catch(IOException e) {
impl.close();
throw e;
}
}
我们可以看到,这些程序中对异常都不是简单的捕获捕获Exception,而是针对不同的异常,进行了不同的处理,这样是用来处理异常的代码显得比较多,也可能是因为这个原因,许多的开发人员都习惯采用简单的方式来处理,只是靠Exception中的信息提供异常的具体信息,这样,的确,减少了代码量,但是也造成了逻辑上的错误。因为在现实世界中,就不存在一刀切的错误处理方式,而是要根据错误的性质和具体情况来判断。
上面所说的是对异常的“处理”,那么对于将异常抛出这种情况,也存在这样的问题,因为并不是所有的出现异常的情况都要集中到最终的程序层来给出处理措施的,有些异常甚至需要使用重试机制来重试一定的次数,等等。
3.3 异常的形式和包含信息不能很好的帮助用户找到引发异常的原因
这个问题是在程序设计的过程中看到的,这个问题比较简单,因为对于开发人员(指API产品)和用户(甚至包括那些只有测试期间会发生的异常,那异常信息就只是由我们自己来看),异常信息显然是不同的,大家都要关注的是关注的是引发异常的原因,但是我们自己最关心的是代码中哪一行引发了这个错误,从而找到和修改程序中的Bug;对API产品来讲,最关心的却是外层调用API的程序(主要是设置API所需要的环境,提供数据,调用API完成所需的功能)哪里出现了错误,包括是不是提供了正确的运行环境,有没有加载正确的类库,提供给API的数据是不是正确等等;对于最终用户(例如对于JSP程序来讲,使用浏览器访问页面的就是最终用户)来讲,他们所关心的是自己的操作是不是正确,比如操作步骤错误、填写表单错误或浏览器(或者其它)设置的错误等等。
异常信息也是程序与用户之间的一个要完成很重要的交流渠道,我们要借助异常信息,来进行辅助调试、功能调用、类库部署以及使用。所以必须审慎的选择异常信息,而不是临时的决定。
常常可以看到这样的程序:
public void method1( ){
if( someCondition )
thow new SomeException( “Exception in method1” );
}
异常如果都这样抛出的话,还有什么意义可言呢?除了可以中断程序,不让错误的程序继续执行下去之外,不能给别人任何有用的信息。当然,假如程序的设计、开发、调试和使用都是一个人的话,他也许心里明白这个someCondition到底是怎么样一个条件,但是我们建议不要这样来写。
3.4 转换旧的错误处理方式
我们知道,在以前的C语言编程里,没有提供结构化异常处理的方法,对运行时错误的处理是通过分支来处理的,例如:
FILE* fp;
if( !fp = fopen( “filename.dat”, “w+b” )) != NULL ){
//...
}
或者下面的代码:
int ret = 0;
ret = doSomeThing( );
使用整数ret,返回错误码。不同的错误码,就代表了(例如有一个错误代码含义表,或者由程序来翻译错误代码以向调用层提供文字的错误信息)。
错误代码这种方式是在没有结构化异常处理机制之前的一种很自然的方式,但是正是因为这种方式存在的不足,才有了异常机制的引入。
错误码的最大问题就是把错误的检查留到了运行期,而异常机制则是在编译时就强制要求对可能发生(当然限于可检查异常)的异常编写预先的捕获和处理代码。
另外一个就是错误代码没有层次性,仅仅是在不同层次的调用之间可能会有一个传递的过程,但是这种传递也是很容易把程序的逻辑搞得很混乱的。
因为错误码是简单的数字,与之有关的有两点,一是所代表的具体含义,二是抛出异常的时机,在OO设计中,没有一个很好的机制来完成这种逻辑层次、逻辑过程的设计。
在面向对象语言中,不提倡使用错误码这种方式来进行错误的处理,一个好的方法就是采用异常来将这种方式转换为面向对象的处理方式,这样做的一个好处就是可以进行很好的OO设计,也有很好的错误处理层次。
4 在OOD的过程中对异常进行规范设计
本部分描述了在Java程序的OO设计阶段应该如何对系统的异常控制进行设计。
4.1 原则
l 按照系统的逻辑对出现的异常进行封装,从而在异常抛出时,可以从异常的文字信息或异常的堆栈信息,分析出引发异常的原因和消除该异常的办法。
l 自定义异常类应从程序的业务逻辑出发,异常类命名,异常信息的选择,异常在哪一个层次进行处理,哪些层次对异常仅仅是抛给调用者,都应该明确体现其在业务逻辑上的意义。
l 在以上原则的基础上,对于大多数异常,在API设计时,应该尽可能抛出给调用层,而不是内部处理。
l 一般情况下,不应该对异常进行太宽范围的处理,而是针对不同的异常进行处理。
在下面的各节,我们将使用一些实际开发的例子对异常的设计来进行说明。
4.2 异常类的命名、包装和层次
异常从本质上来讲,也属于Java类,所以在设计的时候首先要遵循OO中类设计的规范。
在一个工程(对各种IDE而言)中,应该将所有使用到的自定义异常与抛出这些异常的类放在一起,而不是作为一个异常包,包含所有的异常类。应该说,采用一个异常包这种形式是属于程序员(实现者)的思维方式的产物,而将异常分开则是更符合客观世界的逻辑,从Java2的类库中,我们也可以看到,java.lang包内部包含了Exception、NumberFormatException等异常,而java.io包又包含了java.io.Exception、FileNotFoundException等等异常。
对自定义异常的细化也是一个很重要的环节,原则上讲,对于每一类的异常情况,都应该有自己的异常,但是事实上,这个“每一类”是很难去给出一个好的划分方式的。我们可以参考Java2中有关的例子来作为参考:
+--java.lang.Exception
|
+--java.io.IOException
已知的子类: ChangedCharSetException, CharConversionException, EOFException, FileNotFoundException, InterruptedIOException, MalformedURLException, ObjectStreamException, ProtocolException, RemoteException, SocketException, SyncFailedException, UnknownHostException, UnknownServiceException, UnsupportedEncodingException, UTFDataFormatException, ZipException
我们看到,java.io.IOException从java.lang.Exception继承,我们同时也可以看到,从java.io.IOException继承的异常都有了非常明确的含义,针对某种特定的异常,这一点从异常类的名字就可以看出。
尤其重要的是,我们把自己的异常类也作为OO设计(例如使用Rational Rose)中的一个部分,包括在UML的类图中,明确的设计出系统中共包含了什么异常,异常之间又是怎样继承的。
因为异常可能很多,这个里边是不是有一些更好的方法,在UML图中具体描述的还需要进一步考虑。而且,考虑到异常类本身的代码中没有太多的内容,多半都是继承父类,所以,对异常类的描述更加侧重于类之间的层次关系和异常类的命名。
4.3 异常信息的组织
异常信息应该统一定义,为了避免同样的异常,给用户很多种描述信息,使用户无所适从,也为了修改的方便性,我们对于异常信息通过资源文件的方式来实现统一定义。一个可行的方法就是在一个资源类里定义异常的字符串常量,也可以定义资源包,将资源分类,那样可以使单个的资源类不至于太庞大。
按照简化设计的原则,我们对于某些很少使用的异常的信息,可以简单的(简单指不使用常量的形式)来输出,例如
throw new FatalException( “This is a fatal error” );
对于这样的信息,我们在开发的过程中,如果发现会多次的重用这个信息,则应该按照refactor的要求,将其重构为一个异常信息类的常量,这样做,也是简化异常信息类的一个方式。
4.4 异常的抛出时机
通常,在软件OO设计过程中,顺序图是描述一个方法的实现的。在目前的顺序图中,只是一个方法的理想化实现。但是就象我们在本文的最开始部分提到的,正常的流程只是比较小的一个部分,因为运行时很多的情况都可能导致方法出错。
可以理解,如果把所有的异常抛出也很清楚的画到顺序图里,所带来的结果并不是很好,而是整个图看上去很乱,正确的分支和错误的分支相混杂,基本上失去了顺序图的意义。所以,本文并不提倡把异常这样来详细的画在顺序图上。
对于try...catch机制来捕获的异常的说明,我们可以利用(Rational Rose)中,我们可以利用一个小的Note来在原来的顺序图中,对一个方法出现异常的分支进行一个简单的说明,或者,对一个方法,给出一个整体的有关异常抛出的说明。
对于直接在方法的使用throws语句抛出的异常,我们应该在方法的说明部分加以具体描述。
4.5 对异常的处理,例如从异常中恢复、重试等等
在一些程序中,有些异常是可以简单的抛出的,因为那些异常的上下文环境不涉及到一些不可回收资源的释放,但是,大多数情况下,需要对此作出判断是否需要回收,处理代码有可能在catch块中,也有可能是在finally块中。
有些程序甚至需要更加特殊的处理,例如重试网络连接、删除临时文件等等,当然,一个非常常见的例子就是对于数据库程序,必须对当前事务进行回滚。
5 几种比较重要的特殊异常处理
5.1 循环遍历中的异常处理
在循环遍历中,有些异常是需要忽略的,因为循环中的一项有错误,不应该使整个的函数终止。例如以下的程序
public Hashtable getCerts(){
Hashtable ht = new Hashtable();
File f = new File( _certDirPath );
File[] certFiles = f.listFiles();
for( int i = 0; i < certFiles.length; i++ ){
X509Certificate cert = null;
String subject = "";
PublicKey key = null;
try {
FileInputStream fis = new FileInputStream ( certFiles[i] );
CertificateFactory certFact = CertificateFactory.getInstance( "X.509", "BC" );
cert = (X509Certificate)certFact.generateCertificate( fis );
fis.close();
if( cert != null ){
subject = cert.getSubjectDN().toString();
key = cert.getPublicKey();
}
}
catch (NoSuchProviderException ex) {
continue;
}catch (IOException ex) {
continue;
}catch (CertificateException ex) {
continue;
}
if( subject != null && key != null )
ht.put( subject, key );
}
return ht;
}
在这样的程序里,假如目录下的某个文件因为某种原因损坏而无法读出,我们可以看到,程序中只是简单的让程序continue,直接进行下一个循环项。假如文件都发生了错误而无法读出所需的信息,那么返回的HashTable中就会保持运行本方法之前的数据,里边不包含数据。
在这种情况下的异常处理的关键是是否需要中断循环遍历的过程。需要根据程序的具体情况来进行处理。
5.2 多线程中的异常处理
与上边的循环遍历类似的,我们在多线程程序中,也常常是要使用一个循环遍历来重复执行某些后台的操作,通过控制这个循环的条件,达到控制线程的作用。所以这个程序不能简单的因为抛出异常而中断程序的运行,而是应该简单的忽略(对某些特定的异常而言),或者通过其它(例如显示在控制台,记录到日志)等等,对很多Server程序来讲是这样的。
另外一个问题就是在主线程因为异常而结束的处理中,一点要加上终止在主线程中启动的其它线程,否则,对WEB程序而言,这些线程成为孤立线程,除非你重新启动WEB服务器(对于应用程序,重新启动程序就可以了),这些线程不能正常结束。
5.3 空指针异常NullPointerException
在一个 Java 程序员所能遇到的所有异常中,空指针异常属于最恐怖的,这是因为:它是程序能给出的信息最少的异常。例如,不像一个类转型异常,空指针异常不给出它所需要的内容的任何信息,只有一个空指针。此外,它并不指出在代码的何处这个空指针被赋值。在许多空指针异常中,真正的错误出现在变量被赋为空值的地方。为了发现错误,我们必须通过控制流跟踪,以发现变量在哪里被赋值,并确定是否这么做是不正确的。当赋值出现在包中,而不是出现在发生报错的地方时,进程会被明显地破坏。
Null(空标志)在Java中应用非常的广泛,它给程序的设计和编码带来了很大的灵活性,但是从另外的一个角度来讲,使用空标志,程序员有效地掩盖了异常情况出现位置的迹象。谁知道空指针在被丢弃前从方法到方法传递了多远?这只能使得诊断错误以及确定怎样修正它们更加困难。经验证明这种代码经常中断。
5.4 Web程序(jsp,servlet)中的异常
在Web程序(主要是针对JSP而言),我们一般是通过在页面的开始部分设置errorPage的方式,来完成处理的,从而尽可能避免在页面里写繁复的try...catch,但是需要指出的是,对于抛出需要额外处理(释放资源,异常信息转换)的异常,还是需要使用 try...catch的,这样,即便是在catch到异常后直接释放,也可以有机会在finally块中进行额外的处理。
另外就是这种方法在errorPage中,基本上都是采用
String message = exception.getMessage();
out.println( message );
这种方式来处理的,这种方式在 NullPointerException发生时,exception.getMessage()的返回值为 null,不但不能有什么参考价值,反而时异常的引发原因更加不清晰。
5.5 JNI中的异常处理
我们在前边的部分提到,C中多数是通过返回错误码的机制来完成错误处理的,而在C++中,则提供了叫好的异常处理。我们在实现某些JNI程序时,需要将某些C(C++)的程序封装一个JNI的包,从而在Java中,通过本地方法调用类使用。
在JNI 程序中,很容易使用C的处理方式(也就是前边提到的使用返回错误码的方式),遇到问题直接返回一个错误代码,但是考虑到外层程序是从Java中调用,所以我们建议采用抛出异常给Java程序,从而借助Java异常处理的优势,使整个系统的异常处理更加规范化。从C中抛异常给Java的方法,请参考JNI方面资料。
在JNI的C程序端也常常要调用Java方法,如果所调用的Java程序包含了异常的抛出,那么C程序端需要在每次调用了该方法后,调用这样的语句来清除异常:
if( env->ExceptionChecked() )
{
//将捕获到的异常抛给Java虚拟机
env->ThrowNew( env->ExceptionOccurred() );
//清除异常
env->ExceptionClear();
//运行C程序端的错误处理分支
doSomeThing();}
}
需要说明的是,JNI中抛出异常需要异常类在包中,并却,包和类名不能写错,否则会造成Java虚拟机的崩溃。在JNI中,出现内存访问的错误,也会造成虚拟机崩溃,如果程序是运行在Web服务器中,一般的情况下,Web服务器会重启。
6 与异常相关的几个设计和编程问题
6.1 资源的分配与释放
我们知道,在Java中,除了内存之外的资源都不能由GC自动进行收集清理,因而我们必须在程序中显式的进行释放和处理。当然对正常运行的流程来说这比较简单。而对于出现异常的情况,我们需要时刻记住的就是即便出现异常,我们也一样需要释放它们。
请看下边的程序:
Connection conn = null;
Statement st = null;
ResultSet rs = null;
Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
Connection conn = DriverManager.getConnection( "jdbc:odbc:test", "sa", "" );
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery( "select * from table1" );
while( rs.next() ){
String sName = rs.getString(1);
String s = "name=" + sName;
System.out.println( s );
}
rs.close();
st.close();
conn.close();
这段程序非常简单,但是这段程序里包含了很多抛出异常的语句,对这段程序我们需要加上异常的捕获,否则,一是编译不能通过,二是运行期会有很多的错误导致程序因为异常而退出。
这是加上异常处理的程序
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
conn = DriverManager.getConnection( "jdbc:odbc:test", "sa", "" );
st = conn.createStatement();
rs = st.executeQuery( "select * from table1" );
while( rs.next() ){
String sName = rs.getString(“name”);
String s = "name=" + sName;
System.out.println( s );
}
rs.close();
st.close();
conn.close();
}
catch (SQLException ex) {
//...
}catch (ClassNotFoundException ex) {
//...
}catch(Exception ex ){
//...
}
这段程序是可以编译通过的,但是我们看到它有逻辑上的错误,对ClassNotFoundException异常的处理没有问题,因为一开始的加载JDBC驱动的语句如果发生异常,后边的所有的操作都可以不做,但是对于SQLException来讲情况就复杂了,很明显的会有可能发生下边几种错误:
l 当st 对象在执行后边的查询时出错,则conn和st不能释放
l 当rs.getString()时字段名写错,则rs、st、conn不能释放
无需更多的列举,每个人都会想到,将关闭这些资源的语句,从try块中移到finally块中,即:
finally{
rs.close();
st.close();
conn.close();
}
在程序编译时,可以看到是无法编译通过的,这里我觉得我们应该感谢编译器,它阻止了我们犯错误,假如不对此进行阻止,我们看程序,可以发现,假如连接的创建出现错误,那么st和rs理所当然的是 null,我们试图对一个null对象进行close()操作!这个操作所导致的就是一个简单的NullPointerException(),我们将不能从中得到任何错误信息的描述。经过修改,程序可以写成
finally{
try{rs.close();}catch(Exception ex ){}
try{st.close();}catch(Exception ex ){}
try{conn.close();}catch(Exception ex ){}
}
可以看到,我们的catch语句在这里就非常简单,因为我们可以预见到可能会发生的异常――假如异常发生,那么一定是NullPointerException,也就是说,在程序还没有来得及对这个对象进行初始化时发生了异常,所以该对象值为null,对之关闭失败,那就不用关闭了,所以我们可以简单的什么都不做就可以了。
当然还可以这样来写:
finally{
try{
if( rs != null )rs.close();
if( st != null )st.close();
if( conn != null )conn.close();
}catch( Exception ex ){
}
}
笔者更倾向于后面这种方式,因为与前边的代码相比,它很清楚的表达了代码自身的含义,至于还是需要放在 try...catch中,则是编译器的强制要求,我们在使用编译器为我们带来的好处的同时,也不得不接受它带来的一些形式上的强制要求。没有这个强制要求,也许我们会忘记了这个null检查。
对于Socket连接、文件流等等,都和数据库连接很相似,需要非常仔细有关它们的释放。
在这里,我们还讲到了null的检查,下一节我们将对有关null的异常进行探讨。
6.2 从设计和编码来避免出现NullPointerException
看下面的代码:
String s = rs.getString(“name”);
s = s.trim();
编译器需要我们强制进行检查的是SQLException,并不要求我们对字段的值进行NullPointerException,这一点可以理解,因为如果强制要求,那么每段代码都充满了NullPointerException,因为可能出现这样的异常的地方实在是太多了。
另外对于NullPointerException,我们很难指望try...catch这种机制所捕获到的异常能告诉我们有用的信息,因为getMessage()方法得到的信息,是一个简单的”null”。
一个可行的地方是我们在代码里对可能出现的空指针进行检查,这种检查可能很多,但是这也是为了程序的健壮性的一个必须的方法。
例如:
public static boolean method1(Connection conn ,String s1 ){
if( conn == null ){
//处理分支
//有可能是 throw new IllegalArgumentException(“Argument Error!Conection is null!”);
return false;//很多的情况下这样处理
}
if( s1 == null ){
//与上边相似的处理
}
//..正常的处理
}
我们看到对null的检查是很重要的,需要指出的是,我们在程序设计的过程中,常常采用空指针代替异常情况,但这实际上却把控制流限制在方法调用和返回的普通方式,同时也隐藏了异常情况发生的迹象。
在 Java 程序中,异常情况通常是通过抛出异常,并在适当的控制点捕获它们来进行处理。但是经常看到的方法是通过返回一个空指针值来表明这种情况(以及,可能打印一条消息到System.err)。如果调用方法没有明确地检查空指针,它可能会尝试丢弃返回值并触发一个空指针异常。
对NullPointerException的策略是代码中尽量避免使用空标志,但是事实上,许多 Java 类库本身,比如HashTable类和BufferReader
类都用了空标志。当使用这样的类时,您可以通过在执行前,显式检查操作是否将返回空来避免错误。
相关推荐
Java 异常处理习题 Java 异常处理是 Java 编程语言中的一种重要机制,用于处理程序在运行时可能出现的错误或异常情况。下面是关于 Java 异常处理的习题和知识点总结: 一、Java 异常处理关键字 * Java 中用来抛出...
java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理java异常处理
Java异常处理是编程中至关重要的一个环节,它允许开发者优雅地处理程序运行时可能出现的错误,确保程序的健壮性和稳定性。Java中的异常处理机制基于一个独特的概念——异常(Exception),这是一种表示程序运行中...
这个主题,"Java异常处理体系",涉及到Java如何优雅地处理程序运行时的异常,确保程序的健壮性和稳定性。这篇博文链接(已提供但无法直接访问)可能详细解释了Java异常处理的基本概念、机制以及最佳实践。 Java异常...
### 浅析JAVA异常处理机制 #### 一、Java异常处理机制概述 异常处理是Java语言中的一个重要机制,它能够确保程序在遇到不可预料的情况时仍能维持稳定运行。异常处理主要包括三个方面:捕获异常、控制程序流程以及...
Java异常处理是编程中至关重要的一个环节,它确保了程序在遇到错误时能够优雅地运行,而不是突然崩溃。本篇文章将深入探讨Java异常处理的概念、机制以及如何通过代码实例进行有效的异常管理。 Java异常处理的核心...
Java异常处理设计是Java编程中一个至关重要的环节,它直接影响到程序的稳定性和可维护性。在Java中,异常处理是通过try-catch-finally语句块来实现的,主要涉及两大类异常:编译时异常(Checked Exception)和运行时...
Java异常处理的9个最佳实践涵盖了开发者在进行Java编程时应当遵循的一系列规则和方法,以确保代码的健壮性和可维护性。异常处理是编程中的一个重要部分,它能够帮助我们更有效地管理和响应程序运行时的错误情况。 ...
Java异常处理是Java编程语言中的一个关键特性,它允许程序员优雅地处理程序运行时可能出现的错误和不正常状况。在Java中,异常是程序执行过程中出现的问题,它们中断了正常的控制流程,使得程序无法继续按照预期的...
Java异常处理是Java编程中一个至关重要的概念,它关乎到程序的健壮性和稳定性。在Java语言中,异常处理提供了一种机制,使得程序在遇到错误或非正常情况时能够优雅地处理问题,而不是突然崩溃。这个“java高端技术...
通过遵循上述最佳实践,开发者可以编写出更健壮、可维护的Java代码,有效地利用Java异常处理机制来提高程序的稳定性。同时,阅读并理解"Effective Java Exceptions"文档可以帮助进一步深化对Java异常处理的理解。
从给定的文件内容中可以看出,这是一系列Java程序示例,它们用来说明Java异常处理机制。异常处理是Java语言中一个非常重要的部分,它能够帮助程序在遇到错误情况时仍然可以正常运行或者优雅地退出。这些程序通过各种...
java异常练习题,北大青鸟学士后课后练习,主要是异常处理的小测验
以下是对Java异常处理的一些误区和经验总结。 **误区一:过度使用try-catch块** 有些开发者习惯于在每个函数的开始部分都套用try-catch块,以为这样可以捕捉所有可能出现的异常。实际上,这种做法使得代码变得混乱...
零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理零基础学Java异常处理...
### Java异常处理总结 #### 一、Java异常的基础知识 在Java编程中,异常处理是一项极其重要的技术。异常处理能够帮助开发者优雅地处理程序运行时遇到的问题,避免程序崩溃,并提供给用户友好的反馈信息。 **什么...