第十二章 通过异常处理错误
JAVA 的基本理念是:结构不佳的代码不能运行。
发现错误的理想时机是在编译阶段,也就是你试图运行程序之前。
异常处理是 java 中唯一正式的错误报告机制,并且通过编译器强制执行。
1. 概念
- 高傲的程序员们在使用程序库的时候更倾向于:“对,错误也许会发生,但那是别人造成的,不关我的事。”
- “异常”这个词有“ 我对此感到意外 ”的意思。
- 使用异常带来的另一个明显的好处是,他往往能够降低错误代码的复杂度。
2. 基本异常
- 异常情形( exceptional condition )是指阻止当前方法或作用域继续执行的问题。异常情形在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。
- 普通问题是指 在当前环境下能得到足够的信息,总能处理这个错误。
当抛出异常后,有几件事会随之发生:
- 首先,同 java 中的其他对象创建一样,将使用 new 在堆上创建异常对象。
- 之后,当前执行路径被终止, 并从当前环境中弹出对异常对象的引用,此时,异常处理机接管程序。
- 异常处理程序的任务是将程序从错误状态中恢复,以使得程序要么换一种方式运行,要么继续运行下去。
- 抛出异常后,在当前环境下,就不必再为这个问题担心了,他将在别的地方得到处理。
- 异常使得我们可以将每件事都当做一个事务来考虑,而异常可以看护着这些事务的底线,事务是计算机中的合同法。
- 异常最重要的方面之一就是如果发生问题,他们将不允许程序沿着其正常的路径继续走下去。
当时用 new 在堆上创建异常对象,也伴随着存储空间的分配和构造器的调用。所有标准异常都有两个构造器,一个是默认的构造器,一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器。
if(t == null ){ throw new NullPointerException("t == null"); }
- 关键字 throw 将接受异常对象的引用。可以简单地把异常处理看作一种不同而返回机制。
- 异常返回的“ 地点 ” 与 普通方法 调用返回的 “ 地点 ” 完全不同。
- 能够抛出任意类型的 Throwable 对象,它是异常类型的根类,通常对于不同类型的异常,要抛出相应的异常。错误信息存在异常对象内部,上一层通过异常信息来决定如何处理异常。
3 . 捕获异常
要明白异常时如何被捕获的,首先必须理解 监控区域 ( guarded region ) 的概念。他是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
3.1 try 块
如果在方法内抛出了异常,这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕捉异常。在这个块中会尝试各种方法调用,所以称为 try 块,他是跟在 try 关键字 之后的普通程序块。
try{ //// Code }
- 可以把所有动作都放在 try 块里,只需在一个地方就就可以捕获所有代码。
3.2 异常处理程序
抛出的异常必须在某处得到处理,这个地点就是异常处理程序。针对每个要捕获的异常,得准备相应的处理程序,异常处理程序紧跟在 try 之后,以关键字 catch 表示:
try{ //// Code } catch(type id1){ /// Code }
- 每个 catch 字句,看起来像是接受一个且仅接受一个特殊类型参数的方法,有时候可以用到标识符,有时候用不到,但是不可以省略。
- 异常处理程序必须紧跟在 try 块 之后,当异常被抛出后,开始查找与异常类型相匹配的第一个处理程序,并执行。只有匹配的 catch 子句才会被执行
异常处理理论上有两种模型:终止模型、恢复模型
- 假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行,一旦错误被抛出,就表示已经无法挽回。
- 恢复模型意思是异常处理程序的工作是修正错误,然后尝试重新调用的放发,认为第二次可以成功。遇见错误不抛出异常,而是调用方法来修正。
- 长久以来使用的是恢复模型,但是不利于维护,所以慢慢走向了终止模型。
4. 创建自定义异常
- 可以根据自定义异常来表示程序中可能会遇到的特定问题。
- 要定义自定义异常,必须从已有的异常类继承,最好是选择意思相近的异常类继承。建立新的异常类型最简单的方法就是让编译器为你产生默认的构造器。
- 可以通过写入 System.err 将错误发送给标准错误流,这比把信息输出给 System.out 要好, 因为 System.out 可能会重定向。
- 异常与记录日志。
- 异常也是对象的一种,所以可以继续修改异常类,已得到更强的功能,但是程序员一般仅仅看一下跑出的异常类型就不管了,所以对异常添加的其他功能用不上。
5. 异常说明
- java 鼓励人们把方法可能会跑出的异常告知此方法的客户端程序员,他使调用者可以确切知道什么样的代码将会抛出什么样的潜在异常。
- java 提供了相应的语法( 并强制使用这个语法 ),让你可以礼貌的通知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的异常处理,这就是异常说明,他属于方法声明的一部分,紧跟在形式参数列表之后。
异常说明使用了附加关键字 throws ,后面接一个所有潜在异常类型的列表,所以方法定义可能看起来像这样:
void f() throws Toobig ,TooSmall ,DivZero{ /// Code }
- 表示此方法不会抛出任何异常,( 除了从 RuntimeException 继承的异常,他们可以在没有异常的说明的情况下被抛出 )。
- 代码必须与异常说明一直,如果方法里的代码产生了异常但是没有进行处理,编译器就会提醒你。
- 可以声明方法将抛出异常,但是实际上却并不抛出异常,编译器相信这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做可以在以后想抛出异常的时候不用修改已有的代码。
6. 捕获所有异常
可以只写一个异常处理程序来捕获所有类型的异常。通过捕获异常类型的基类 Exception 就可以做到这一点。(事实上还有其他的基类,但是 Exception 是同编程活动相关的基类 )
catch(Exception e){ System.out.println("Caught an exception"); }
- 这将捕获所有类型的异常,所以最好把他放在处理程序列表的末尾,以防止它抢在其他处理程序之前先把异常捕获了。
Exception 是与编程有关的所有异常类的基类,所以他不会包含太多的具体信息,可以调用它从其基类 Throwable 集成的方法:
- String getMessage() 、 String getLocalizedMessage() 获取详细信息
- String toString() , 返回会 Throwable 的简单描述
- void printStackTrace() 、 void printStackTrace ( PrintfStream ) 、 void printStackTrace ( java.io.PrintWriter ) ,打印 Throwable 调用栈轨迹。调用栈显示了 “ 把你带到异常抛出地点 ”
- Throwable fillInStackTrace() 、 记录帧的当前状态。
也可以调用从 Object 继承来的方法
- getClass() , 返回一个表示此对象类型的对象
- getName() ,查询这个 Class 对象包含信息的名称。
- getSimpleName() , 只产生类名称的方法
6.1 栈轨迹
- printStackTrace() , 提供的信息,可以通过 getStackTrace() , 方法来直接访问,返回一个由栈轨迹中的元素所构成的数组。
6.2 重新抛出异常
有时候希望把刚捕获的异常重新抛出,尤其是在捕获所有异常的时候。
catch (Exception e){ System.out.println("An exception was throw"); throw e; }
- 重新抛出异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块后的语句将被忽略。
- 想要更新抛出异常的信息,需要调用 fillInStackTrace() 方法。
- 永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心,他们都是用 new 在对象创建的对象,所以垃圾回收器会自动把他们清理掉。
6.3 异常链
- 常常希望在捕获一个异常后抛出另一个异常,并希望把原始异常的信息保存下来,这被称为异常链。
- Throwable 的子类构造器中都可以接受一个 cause ( 因由 ) 对象 作为参数,这个 cause 就表示原始异常,这样通过将原始异常发送给新的异常,就可以通过异常链追踪到异常最初发生的位置。
- Throwable 的子类中 ,只有三种基本的异常类提供了带 cause 参数的构造器,他们是 Error 、 Exception 、 RuntimeException 。如果要把其他类型的异常链接起来,应该使用 initCause() 方法而不是 构造器。
7. java 标准异常
Throwable 这个 java 类被用来表示任何可以作为异常被抛出的。Throwable 可以将对象分为两种类型 ( 从 Throwable 继承而得到的类型 ):
- Error 用来表示编译时和系统错误
- Exception 是可以被抛出的基本类型,在 java 类库、用户方法以及运行时故障中都有可能抛出 Exception 型异常,所以 java 程序员最关心的是 Exception
- 对异常来说,最关键的是理解概念以及如何使用
7. 1 RuntimeException
- 如果对 null 引用进行调用,会自动抛出 NullPointerException 的异常。
- 属于运行时异常的类型有很多,所以不必在异常说明中把他们都列出来。
- 从 RuntimeException 继承来的异常,被称为 “ 不受检查的异常 ” ,这种异常属于错误,会自动被捕获。
- 如果 RuntimeException 没有被捕获直接到达 main() ,那么在程序退出前将调用 printStackTrace() 方法。
只能在代码中忽略 RuntimeException ( 及其子类 ) 类型的异常。其他类型的异常处理都是由编译器强制实施的,究其原因, RuntimeException 代表的是 编程错误:
- 无法预料的错误,比如你控制范围之外传递过啊里的 null 引用
- 作为程序员,应该在代码中进行检查的错误。
- 不能把 java 的异常处理机制 当做是单一用途的工具。
8. 使用 finally 进行清理
对于一些代码,可能会希望无论 try 块中的异常是否被抛出,他们都可以被执行。这通常是用于内存回收之外的情况。为了达到这个效果,可以再异常处理程序后面加上 finally 字句:
try{ //// Code }catch( A a1){ //// Code } finally{ /// Code }
- 无论异常是否被输出, finally 字句 总是可以被执行
8.1 finally 用来做什么
- 当要把除内存之外的资源恢复到他们的初始状态时,就要用到 finally 子句。这种需要清理的资源包括:已经打开的文件或者网络连接,在屏幕上画的图形,甚至可以使外部世界的某个开关。
- 当涉及 break 和 continue 语句的时候,finally 子句也会得到执行。 如果把 finally 子句 与 带标签的 break 和 continue 配合使用, 在 java 中就没必要使用 goto 语句了。
在 return 中使用 finally
- 因为 finally 子句 总是会执行,所以在一个方法中,可以从多个点返回,并且可以保证重要的清理工作仍旧会执行。
- 在 finally 类内部,从何处返回无关紧要 。
缺憾:异常丢失
9. 异常的限制
- 当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。
- 派生类构造器不能捕获基类构造器抛出的异常。
- 派生类方法可以不抛出任何异常,即使他是基类所定义的异常。
10. 构造器
- 如果异常发生了。那么所有东西都能被正确的清理吗?
- 构造器会把对象设置成安全的初始状态,但还会有别的动作。
11. 异常匹配
- 抛出异常的时候。异常处理系统会按照代码的书写顺序找出 “ 最近 ” 的处理程序。找到匹配的处理程序之后,他就认为异常将得到处理,然后就不再继续查找。
- 查找的时候,并不要求抛出的异常同处理程序所声明的异常完全匹配,。派生类的对象也可以匹配其基类的处理程序。
12. 其他可选方式
- 异常处理就像一个活门( trap door ),使你放弃程序的正常执行序列。
- 开发异常处理的初衷是为了方便程序员处理错误。
- 异常处理的一个重要原则是,只有在你知道如何处理的情况下才捕获异常。
- 还有就是,把错误处理的代码同错误发生的地点分离,这使你能在一段代码中专注于要完成的
- 被检查的异常 使这个问题变得有些复杂,因为他们强制你在可能还没准备好处理错误的时候被迫加上 catch 语句,这就导致了 吞噬则有害 ( harmful if swallowed ) 的问题。
- java 和 c++ 属于 强静态类型语言。
13. 异常使用指南
应该在下列情况下使用异常:
- 在恰当的级别处理问题。(再知道该怎么处理的情况下才捕获异常)。
- 解决问题并且重新调用产生异常的方法。
- 进行少许修补,然后绕过异常发生的地方继续执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做的事尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化。
- 让类库和程序更安全。
异常处理的优点之一就是它使得你可以再某处集中精力处理你要解决的问题。
报告功能是异常的精髓所在。
由本人从 Thinking in java ( java 编程思想 ) 整理而来