JAVA异常处理是程序开发的一个重要内容,异常处理的好坏关系到系统的健壮性和稳定度。异常处理看起来只有几个常用语句,故有些开发人员常常会对异常处 理轻视和在使用上思路模糊。近期笔者在一个开发项目中就体验到轻视异常处理的惨痛教训,因为对异常没有处理好,后果是严重影响系统稳定性。因此,笔者认为 异常处理并不是表面看起来的那么简单。本文分享在此项目过程中对异常处理的一些看法。
一. 什么是异常
在JAVA程序运行时,我们常常会出现一些非正常的现象,这种情况称为运行错误。根据其性质可以分为错误和异常。JAVA用面向对象的方法处理异常,首先 会建立类的层次。类 Throwable位于这一类层次的最顶层,只有它的后代才可以作为一个异常被抛弃。类Throwable有两个直接子类:Error和 Exception。
一般来说错误最常见的有程序进入死循环,内存泄漏等。这种情况,程序运行时本身无法解决,只能通过其他程序干预。JAVA对应的类为Error类。Error类对象由JAVA虚拟机生成并抛弃(通常JAVA程序不对这类异常进行处理)。
异常是程序执行时遇到的非正常情况或意外行为。一般以下这些情况都可以引发异常:代码或调用的代码(如共享库)中有错误,操作系统资源不可用,公共语言运 行库遇到意外情况(如无法验证代码)等等。常见的有数组下标越界,算法溢出(超出数值表达范围),除数为零,无效参数、内存溢出等。这种情况不像错误类那 样,程序运行时本身可以解决,由异常代码调整程序运行方向,使程序仍可继续运行直至正常结束。
JAVA对应的类为Exception类。Exception类对象是JAVA程序处理或抛弃的对象。它有各种不同的子类分别对应于不同类型的异常。 JAVA编译器要求程序必须捕获或声明所有的非运行时异常,但对运行时异常可以不做处理。其中类RuntimeException代表运行时由JAVA虚 拟机生成的异常,原因是编程错误。其它则为非运行时异常,原因是程序碰到了意外情况,如输入输出异常IOException等。
二. 异常处理程序的功效
当在程序运行过程中发生的异常事件,这些异常事件的发生将阻止程序的正常运行。为了加强程序的稳定性,程序设计时,必须考虑到可能发生的异常事件并做出相应的处理。因此, 异常处理程序就是能够让系统在出现异常的情况下恢复过来的程序。
JAVA通过面向对象的程序来处理异常。在一个程序的运行过程中,如果发生了异常,则这个程序生成代表该异常的一个对象,并把它交给运行时系统,运行时系 统寻找相应的代码来处理这一异常。我们把生成异常对象并把它提交给运行时系统的过程称为抛出异常(Throw)。异常抛出后,运行时系统从生成对象的代码 开始,沿程序的调用栈逐层回溯查找,直到找到包含相应处理的程序,并把异常对象交给该程序为止,这个过程称为捕获异常(Catch)。
为了使异常处理更出色地发挥它的功效,程序员需要对所有可能发生的异常,预制各式各样的异常类和错误类。它们都是从抛出异常类Throwable继承而来的,它派生出两个类Error和Exception。
由Error派生的子类命名为XXXError,其中词XXX是描述错误类型的词。由Exception派生的子类命名为XXXException,其中 词XXX是描述异常类型的词。Error类处理的是运行使系统发生的内部错误,是不可恢复的,唯一的办法是终止运行程序。因此,一般来说开发人员只要掌握 和处理好Exception类就可以了。对于运行时异常RuntimeException,我们没必要专门为它写一个异常控制器,因为它们是由于编程不严 谨而造成的逻辑错误。只要出现终止,它会自动得到处理。需要开发人员进行异常处理的是那些非运行期异常。
三、异常处理的两种思路
JAVA异常处理的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然后在另一个地方对待来自那个代码内部的错误。我们只需要在那个可能发生 异常的地方设置“监视区”,我们对此区域日夜监视着,通常它是一个语句块。同时我们还需要在另一个地方设置处理问题模块,如“异常处理模块”或者“异常控 制器”。这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正异常的代码分隔开。一般情况下,会让用于读取、写入以及调试的代码会变得更富有 条理。
一般来说有两种思路处理异常。第一种将含有异常出口的程序直接放到try块中,然后由紧随其后的catch块捕捉。JAVA由try…catch语法来处 理异常,将关联有异常类的程序包含在try{}程序块中,catch(){}关键字可以使用形参,用于和程序产生的异常对象结合。当调用某个程序时,引起 异常事件发生的条件成立,便会抛出异常,原来的程序流程将会在此程序处中断,然后try模块后紧跟的catch中的形参和此异常对象完成了结合,继而进入 了catch模块中运行。
这里引用一个最简单的例子来说明: int myMethod(int dt)...{int data = 0; try{ int data = isLegal(dt); }catch(LowZeroException e)...{ System。out。println("发生数据错误!");} return data;}
第二种是不直接监听捕捉被引用程序的异常,而是将这个异常关联传递给引用程序,同时监听捕捉工作也相应向上传递。
四. 解读五个异常处理语句的应用教训
笔者结合本次项目教训谈谈JAVA异常处理的五个关键语句:try,catch,throw,throws,finally。希望能与大家分享在本次项目开发遇到的问题和总结一些经验教训。
4.1 Try和catch的教训
try语句用{}指定了一段代码,该段代码可能会抛弃一个或多个异常。catch语句的参数类似于程序的声明,包括一个异常类型和一个异常对象。异常 类型必须为Throwable类的子类,它指明了catch语句所处理的异常类型,异常对象则由运行时系统在try所指定的代码块中生成并被捕获,大括号 中包含对象的处理,其中可以调用对象的程序。
JAVA运行时系统从上到下分别对每个catch语句处理的异常类型进行检测,直到找到类型相匹配的catch语句为止。这里类型匹配指catch所处理 的异常类型与生成的异常对象的类型完全一致或者是它的父类。因此,catch语句的排列顺序应该是从特殊到一般。也可以用一个catch语句处理多个异常 类型,这时它的异常类型参数应该是这多个异常类型的父类,程序设计中要根据具体的情况来选择catch语句的异常处理类型。
异常被异常处理程序捕获和处理,异常处理程序紧接在try块后面,且用catch关键字标记,因此叫做“catch块”。如果一个程序使用了异常规范,我 们在调用它时必须使用try-catch结构来捕获和处理异常规范所指示的异常,否则编译程序会报错而不能通过编译。这正是JAVA的异常处理的杰出贡 献,它对可能发生的意外及早预防从而加强了代码的健壮性。
在这次项目中得到一个教训是不要用一个catch语句捕获所有的异常和试图处理所有可能出现的异常。一个程序中可能会产生多种不同的异常,我们可以设置多 个异常抛出点来解决这个问题。异常对象从产生点产生后,到被捕捉后终止生命的全过程中,实际上是一个传值过程,所以我们需要根据实际需要来合理的控制检测 到异常的个数。catch语句表示我们预期会出现某种异常,而且希望能够处理该异常。我们建议在catch语句中应该尽可能指定具体的异常类型,必要时使 用多个catch,用于分别处理不同类的异常。
实际上绝大多数异常都直接或间接从JAVA.ang.Exception派生。例如我们想要捕获一个最明显的异常是SQLException,这是 JDBC操作中常见的异常。另一个可能的异常是IOException,因为它要操作 OutputStreamWriter。显然,在同一个catch块中处理这两种截然不同的异常是不合适的。如果用两个catch块分别捕获 SQLException和IOException就要好多了。这就是说,catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的 Exception类。
在此项目另一个教训是初级开发人员总喜欢把大量的代码放入单个try块,这个坏习惯使我们在测试和分析问题过程中花费了大量的时间。把大量的代码放入单个 try块,然后再在catch语句中声明Exception,而不是分离各个可能出现异常的段落并分别捕获其异常。这种做法为分析程序抛出异常的原因带来 了困难,因为一大段代码中有太多的地方可能抛出Exception。程序的条理性和可阅读性也会变得非常差,因此我们需要尽量减小try块的体积。
异常处理中还有一种特殊情况---RuntimeException异常类,这个异常类和它的所有子类都有一个特性,就是异常对象一产生就被JAVA虚拟 机直接处理掉,即在程序中出现throw 子句的地方便被虚拟机捕捉了。因此凡是抛出这种运行时异常的程序在被引用时,不需要用try…catch语句来处理异常。
异常处理语句的一般格式是:
try ...{// 可能产生异常的代码 }catch (异常对象 e) ...{ //异常 e的处理语句 }catch (异常对象 e1) ...{ //异常 e的处理语句 }catch (异常对象 e2) ...{ //异常 e的处理语句 }
4.2 解读Throw和throws区别
在使用异常规范的程序声明中,开发人员使用throw语句来抛出异常,throw总是出现在函数体中。程序会在throw语句后立即终止,它后面的语句执行不到,然后在包含它的所有try块中从里向外寻找含有与其匹配的catch子句的try块。
throw语句的格式为:
throw new XXXException();
由此可见,throw语句抛出的是XXX类型的异常的对象(隐式的句柄)。而catch控制器捕获对象时要给出一个句柄 catch(XXXException e)。
如果一个Java程序遇到了它不能够处理的情况,那么它可以抛出一个异常:一个程序不仅告诉Java编译器它能返回什么值,还可以告诉编译器它有可能产生 什么错误。JAVA为了使开发人员准确地知道要编写什么代码来捕获所有潜在的异常,采用一种叫做throws的语法结构。它用来通知那些要调用程序的开发 人员,他们可能从自己的程序里抛出什么样的异常。这便是所谓的“异常规范”,它属于程序声明的一部分。
throw 子句用来抛出异常,而throws子句用来指定异常。throw 的操作数是Throwable所有派生类,Throwable的直接子类是Exception(应捕获的问题,应进行处理)与Error(重大系统问题, 一般不捕获)。抛出异常抛出点有try{}块、, try{}块某个深层嵌套的作用域、try{}块某个深层嵌套的程序中。简单说throws是指定throw抛出的异常。
throws总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常。对大多数Exception子类来说,JAVA 编译器会强迫你声明在一个成员函数中抛出的异常的类型。如果你想明确地抛出一个RuntimeException,你必须用throws语句来声明它的类 型。
例如
void f() throws tooBig, tooSmall, divZero { 程序体}
若使用下述代码:
void f() [ // 。。。
它意味着不会从程序里抛出异常。
4.3. 巧妙应用finally使出口统一
异常改变了程序正常的执行流程。这个道理虽然简单,却常常被人们忽视。我们在这次项目中就遇到这样的情况,就是无论一个异常是否发生,必须执行某些特定的 代码。比如文件已经打开,关闭文件是必须的。再如在程序用到了Socket、JDBC连接之类的资源,即使遇到了异常,正常来说是也要正确释放占用的资 源。
但是,在try所限定的代码中,当抛弃一个异常时,其后的代码不会被执行。在catch区中的代码在异常没有发生的情况下也不会被执行。为了无论异常是否 发生都要执行的代码,为此,JAVA提供了一个简化这类操作的关键词finally,也就是无论catch语句的异常类型是否与所抛弃的异常的类型一致, finally所指定的代码都要被执行。Finally保证在try/catch/finally块结束之前,执行清理任务的代码有机会执行,它提供了统 一的出口。
五. 切莫轻视异常处理
常常会有一些程序员习惯在编程时拖延或忘记异常处理程序的编写。因为轻视异常这一坏习惯是如此常见,它甚至已经影响到了JAVA本身的设计。代码捕获了异 常却不作任何处理,可以算得上JAVA编程中的杀手。从问题出现的频繁程度和祸害程度来看,如果你看到了出现异常的情况,可以百分之九十地肯定代码存在问 题。
最好的方法是在进行系统设计就把异常处理融合在系统中,若系统一旦实现,就很难添加异常处理功能。因此从项目一开始就应该着手进行异常处理,必须投入大精力把异常处理的策略融合到软件产品中。