10.异常处理

本章要点

  • 异常的定义和概念
  • java异常机制的优势
  • 使用try...catch来捕获异常
  • java异常类的继承体系
  • 异常对象的常用方法
  • finally块的作用
  • 异常处理的合理嵌套
  • Checked异常和Runtime异常
  • 使用throws声明异常
  • 使用throw抛出异常
  • 自定义异常
  • 异常链和异常转译
  • 异常的跟踪栈信息
  • 异常的处理规则

异常机制已经成为判断一门编程语言是否成熟的标准,除了传统的像C语言没有提供异常机制之处,目前主流的编程语言如Java,C#,Ruby,Python等都提供了成熟的异常机制。异常机制可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。

java的异常机制主要依赖于try,catch,finally,throw和throws五个关键字,其中try关键字后紧跟一个花括号括起来的代码块(花括号不可省略)简称为try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表面该catch块用于处理这种类型的代码块。多个catch块后还可以跟一个finally块,finally块用于回收再try块里打开的物理资源,异常机制会保证finally块总被执行。throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常,而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体异常对象。

我们希望所有错误都可以在编译阶段被发现,就是在试图运行程序之前就能排除所有错误,但这是不现实的,余下的问题必须在运行期间得到解决。java将异常分为两种,Checked异常和Runtime异常,java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所以的Checked异常。而Runtime异常则无须处理。

10.1 异常概述

对于构造大型,健壮,可维护的应用而言,错误处理是整个应用需要考虑的重要方面,曾经有一个教授告诉我:我们国内的程序员做开发时,往往只做“对”的事情!他的这句话有很深的遗憾:程序员开发程序的过程,是一个创造的过程,这个过程需要有全面的考虑,仅做“对”的事情是远远不够的。

对于上面的错误处理机制,主要有如下两个确定:

  • 无法穷举所有异常情况:因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
  • 错误处理代码和业务实现代码混杂:这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。

10.2 异常处理机制

java的异常处理可以让程序具有更好的容错性,程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

10.2.1 使用try...catch 捕获异常

当java运行时环境收到异常对象时,会寻找能处理异常对象的catch块,如果找到合适的catch块并把该对象交给该catch块处理,那这个过程被称为不火异常;如果java运行时环境找不到捕获异常的catch块,则运行时环境终止,java程序也将退出。

提示:

不管程序代码块是否处于try块中,甚至包括catch块中代码,只要执行该代码块时出现了异常,系统都会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,java运行时环境肯定找不到处理该异常的catch块,程序肯定在此退出这就是前面看到的例子程序在遇到异常时退出的情形。

10.2.2 异常类的继承体系

当java运行时环境接收到异常对象时,如何为该异常对象寻找catch块呢?注意上面Gobang程序中catch关键字的形式:(Exception e),这意味着每个catch块都是专门用于处理该异常类及其子类的异常实例。

当java运行时环境接收到异常对象后,会一次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,java运行时环境将调用该catch块来处理该异常;否则再次拿该异常对象和下一个catch块里的异常类进行比较。

try块后可以有多个catch块。try块后使用多个catch块是为了针对不同异常类提供不同的异常处理方式。当系统发生不同意外情况时,系统会生成不同的异常对象,java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。

通过在try块后提供多个catch块可以无须在异常处理块中使用if,switch判断异常类型,但依然可以针对不同异常类型提供相应的处理逻辑,从而提供更细致,更有条理的异常处理逻辑。

通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行。除非在循环中使用了continue开始下一次循环,下一次循环又重新运行了try块,这才可能导致多个catch块被执行。

注意:

try块与if语句不一样,try块后的花括号{}不可以省略,即使try块里只有一行代码,也不可省略这个花括号。与之类似的,catch块后的花括号{}也不可以省略。还有一点需要指出:try块里声明的变量是代码块内局部变量,它只在try块内有效,catch块中不能访问该变量。

java把所有非正常情况分成另种:异常(Exception)和错误(Error),它们都是继承Throwable父类。

Error错误,一般是指虚拟机相关的问题,如系统崩溃,虚拟机出错误,动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

java运行时的异常处理逻辑可能有如下几种情形:

  • 如果运行该程序时输入的参数不够,将会发生数组越界异常,java运行时使用IndexOutOfBoundsException对应的catch块处理该异常;
  • 如果运行该程序输入额参数不是数字,而是字母,将发生数字格式异常,java运行时将调用NumberFormatException对应的catch块处理该异常。
  • 如果运行该程序输入的第二个参数是0,将发生除0异常,java运行时将调用ArithmeticException对应的catch块处理该异常。
  • 如果程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,java运行时将调用Exception对应的catch块处理该异常。

注意:

进行异常捕获时,一定要记住先捕获小的异常,再捕获大的异常。

10.2.3 访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,可以通过调用catch后异常形参的方法来获得。当java运行时决定调用某个catch块来处理该异常对象时,会将该异常对象赋给catch块后的异常参数,程序就可以通过该参数来获得该异常的相关信息。

所有异常对象都包含了如下几个常用方法:

  • getMessage():返回该异常的详细描述字符串。
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
  • printStackTrace(PritStream s):将该异常的跟踪栈信息输出到指定输出流。
  • getStackTrace():返回该异常的跟踪栈信息。

10.2.4 使用finally回收资源

有些时候,程序在try块里打开了一些物理资源(例如数据库连接,网络连接和磁盘文件等),这些物理资源都必须显式回收。

提示:

java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。

为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。

异常处理语法结构中只有try块是必须的,也就是说如果没有try块,则不能有后面的catch块和finally块;catch块和finally块都是可选的,但至少出现其中之一,也可以同时出现;可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;但不能只有try块,既没有catch块,也没有finally块;多个catch块必须位于try块之后,finally块必须位于所有catch块之后。

注意:

除非在try块,catch块中调用了退出虚拟机的方法,否则不管在try块,catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。

尽量避免在finally块里使用return或throw等导致方法终止的语句,否则可能出现一些很奇怪的情况。

10.2.5 异常处理的嵌套

10.3 Checked异常和Runtime异常体系

java的异常被分为两个类:Checked异常和Runtime异常(运行时异常)。所有runtimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。

只有java语言提供了Checked异常,其他语言都没有提供Checked异常。java认为Checked异常都是可以被处理(修复)的异常,所以java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。

Checked异常体现了java的设计哲学:没有完善错误处理的代码根本不会被执行!

对于Checked异常的处理方式有两种:

  • 当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常,然后在对应的catch块中修补该异常。
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕捉Runtime异常,也可以使用try...catch块来捕捉Runtime异常。

提示:

只有java语言提供了Checked异常,Checked异常体现了java严谨性,它要求程序员必须注意该异常:要么显式声明抛出,要么显式捕捉它并处理该异常,这是一种非常严谨的涉及哲学,可以增加程序的健壮性。问题是:大部分的方法总是不能明确地知道如果处理异常,这就只能声明抛出该异常,而这种情况又是如此普遍,所以checked异常降低了程序开发的生产率和代码的执行效率。关于Checked异常的优劣,在java领域是一个备受争论的问题。

10.3.1 使用throws声明抛出异常

使用throws声明抛出异常的思路是:当前方法不知道应该如何处理这种类型的异常,该异常应该由上一级调用者处理,如果main方法也不知道应该如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是:打印异常跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。

10.4 使用throw抛出异常

当程序出现错误时,系统会自动抛出异常;除此以外,java也允许程序自行抛出异常,自行抛出异常使用throw语句完成。

10.4.1 抛出异常

异常是一种很“主观”的说法,以下雨为例,假设大家约好明天去爬山郊游,如果第二天下雨了,这种情况会打破既定计划,就属于一种异常;但对于正在企盼天降甘霖的农民而言,如果第二天下雨了,他们正好随雨追肥,这就完全正常。

很多时候,系统是否要抛出异常,可能需要根据应用的业务需求来决定,如果程序中的数据,执行与既定的也无需求不符,那这就是一种异常。这种由于与业务需求不符而产生的异常,必须由程序员来决定抛出,系统无法抛出这种异常。

如果需要在程序中自行抛出异常,应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。。

10.4.2 自定义异常类

通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常包含了该异常的有用信息。所以在选择抛出什么异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。

用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时需要提供两种构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的详细说明(也就是异常对象的getMessage方法的返回值)。

10.4.3 catch和throw同时使用

前面介绍的异常处理方式有两种:

  • 在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常。
  • 该方法签名中声明抛出该异常,将该异常完全交给调用者处理。

实际应用中往往需要更复杂的处理方式:当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,异常出现的当前方法中,程序只对异常进行部分处理,,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,这样就可以让该方法的调用者也能捕获到异常。

为了实现这种靠多个方法协作处理同一个异常的情形,可以通过在catch块中结合throw来完成。

注意:

这种catch和throw结合使用的情况在大型企业级应用中非常常用,企业级应用对异常的处理通常需分成2个部分:一、应用后台需要通过日志来记录异常发生的详细情况;二、应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。

10.4.4 异常链

对于真实的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会跨层访问。

当业务逻辑层访问持久层出现SQLexception异常时,程序不应该把底层的SQLException异常传到用户界面,原因有如下两个:

  • 对于正常用户而言,他们不想看到底层SQLException,SQLException对他们使用该系统没有任何帮助。
  • 对于恶意用户而言,将SQLException暴露出来不安全。

无论如何,把底层的原始异常直接传给用户是一种不负责任的表现。通常情况的做法是:程序先捕获原始的异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。

这种把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。

这种把捕获一个异常然后接着抛出另一个异常,并把原始异常的信息保存下来的是一中典型的链式处理(职责链模式),也被称为“异常链”。

10.5 java的异常跟踪栈

异常对象的printStackTrace方法用于打印异常的跟踪栈信息,根据printStackTrace方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程。

在面向对象的编程中,大多数复杂操作都会被分解成一系列方法调用。这是因为:实现更好的可重用性,将每个可重用的代码单元定义成方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个对象来共同实现,在最终编程模型中,很多对象将通过一系列方法调用来实现通信,执行任务。

所以 面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新跑出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者......直至最后出传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。

提示:

虽然printStackTrace()方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它,而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。

10.6 异常处理规则

成功的异常处理应该达到如下四个目标:

  • 使程序代码混乱最小化。
  • 捕捉并保留诊断信息。
  • 通知合适的人员。
  • 采用合适的方式结束异常活动。

10.6.1 不要过度使用异常

不可否认,java的异常机制确实方便,但滥用异常机制也会带来一些负面影响。过度使用异常主要有两个方面:

  • 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有错误处理。
  • 使用异常处理来代替流程控制。

熟悉了异常使用方法,程序员可能不再愿意编写繁琐的错误处理代码,而是简单的抛出异常。实际上这样做是不对的,对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性。对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只对外部的,不能确定和预知的运行时错误才使用异常。

10.6.2 不要使用过于庞大的try块

10.6.3 避免使用Catch All语句

  • 所有异常都采用相同的处理方式,这将导致无法对不同异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制,这是得不偿失的做法。
  • 这种捕获方式可能将程序中的错误,Runtime异常都应该导致程序终止的情况全部捕获到,从而“压制”了异常。如果出现了一些“关键”异常,那个异常也会被”静悄悄“地忽略。

10.6.4 不要忽略捕获到的异常

  • 处理已换成那个
  • 重新抛出新异常
  • 在合适的层处理异常
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值