Java异常机制的最佳实践(中)

简介: Java异常机制的最佳实践(中)

6 捕获所有异常

6.1 如何使用 Exception 类型

可以只写一个异常处理程序来捕获所有类型的异常。

通过捕获异常类型的基类 Exception 即可:

image.png

最佳实践

它会捕获所有异常,所以最好把它放在处理程序列表的末尾,以防它抢在其他处理程序之前先把异常捕获了。

从 Throwable 继承的方法

因为 Exception 是与编程有关的所有异常类的基类,不含太多具体信息,但可以调用它从其基类 Throwable 继承的方法:

  • String getMessage()
  • image.png
  • 用来获取详细信息,或用本地语言表示的详细信息。
  • image.png
  • 返回对 Throwable 的简单描述,要是有详细信息的话,也会把它包含在内。
  • image.png
  • 打印 Throwable 和 Throwable 的调用栈轨迹。调用栈显示了“把你带到异常抛出地点”的方法调用序列。其中第一个版本输出到标准错误,后两个版本允许选择要输出的流。
  • image.png
  • 用于在 Throwable 对象的内部记录栈帧的当前状态。这在程序重新抛出错误或异常时很有用。

此外,也可以使用 Throwable 从其基类 Object(也是所有类的基类)继承的方法。对于异常来说,getClass)也许是个很好用的方法,它将返回一个表示此对象类型的对象。然后可以使用 getName)方法查询这个 Class 对象包含包信息的名称,或者使用只产生类名称的 getSimpleName() 方法。

使用案例

image.png

输出为:

image.png

可以发现每个方法都比前一个提供了更多的信息一一实际上它们每一个都是前一个的超集。

多重捕获

如果有一组具有相同基类的异常,你想使用同一方式进行捕获,那你直接 catch 它们的基类型。但是,如果这些异常没有共同的基类型,在 Java 7 之前,你必须为每一个类型编写一个 catch:

image.png

通过 Java 7 的多重捕获机制,你可以使用“或”将不同类型的异常组合起来,只需要一行 catch 语句:

image.png

或者以其他的组合方式:

image.png

这对书写更整洁的代码很有帮助。

栈轨迹

printStackTrace() 方法所提供的信息可以通过 getStackTrace() 方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一桢。元素 0 是栈顶元素,并且是调用序列中的最后一个方法调用(这个 Throwable 被创建和抛出之处)。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。下面的程序是一个简单的演示示例:

image.png

输出为:

image.png

这里,我们只打印了方法名,但实际上还可以打印整个 StackTraceElement,它包含其他附加的信息。

重新抛出异常

有时希望把刚捕获的异常重新抛出,尤其是在使用 Exception 捕获所有异常的时候。既然已经得到了对当前异常对象的引用,可以直接把它重新抛出:

image.png

重抛异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。此外,异常对象的所有信息都得以保持,所以较高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

若只是把当前异常对象重新抛出,那么

  • printStackTrace() 方法显示的将是原来异常抛出点的调用栈信息,而非重新抛出点的信息。
  • image.png

要想更新该信息,可调用 filInStackTrace() 方法,这将返回一个 Throwable 对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。

image.png

就像这样:

image.png

输出为:

image.png

调用 fillInStackTrace 的那一行就成了异常的新发生地。

有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用 filInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息:

image.png

输出为:

image.png

最后那个异常仅知道自己来自 main(),而对 f() 一无所知。

永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心。它们都是用 new 在堆上创建的对象,所以垃圾回收器会自动把它们清理掉。

精准的重新抛出异常

在 Java 7 前,若遇到异常,则只能重新抛出该类型的异常。这导致在 Java 7 中修复的代码不精确。所以在 Java 7 前,这无法编译:

image.png

因为 catch 捕获了一个 BaseException,编译器强迫你声明 catcher() 抛出 BaseException,即使它实际上抛出了更具体的 DerivedException。从 Java 7 开始,这段代码就可以编译,这是一个很小但很有用的修复。

异常链

想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。

JDK1.4 前,程序员必须自己编写代码保存原始异常的信息。现在所有 Throwable 的子类在构造器中都可接收一个 cause 对象作为参数,表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。

在 Throwable 的子类中,只有三种基本的异常类提供了带 cause 参数的构造器:

  • Error(用于 Java 虚拟机报告系统错误)
  • Exception
  • RuntimeException

如果要把其他类型的异常链接起来,应该使用 initCause() 而非构造器。

运行时动态地向 DymamicFields 对象添加字段

image.png

image.png

输出为:

image.png

每个 DynamicFields 对象都含有一个数组,其元素是“成对的对象”。第一个对象表示字段标识符(一个字符串),第二个表示字段值,值的类型可以是除基本类型外的任意类型。当创建对象的时候,要合理估计一下需要多少字段。当调用 setField() 方法的时候,它将试图通过标识修改已有字段值,否则就建一个新的字段,并把值放入。如果空间不够了,将建立一个更长的数组,并把原来数组的元素复制进去。如果你试图为字段设置一个空值,将抛出一个 DynamicFieldsException 异常,它是通过使用 initCause() 方法把 NullPointerException 对象插入而建立的。

至于返回值,setField() 将用 getField() 方法把此位置的旧值取出,这个操作可能会抛出 NoSuchFieldException 异常。如果客户端程序员调用了 getField() 方法,那么他就有责任处理这个可能抛出的 NoSuchFieldException 异常,但如果异常是从 setField0 方法抛出,这种情况将被视为编程错误,所以就使用接受 cause 参数的构造器把 NoSuchFieldException 异常转换为 RuntimeException 异常。

主方法中的 catch 子句看起来不同 - 它使用相同的子句处理两种不同类型的异常,并结合“或(|)”符号。此 Java 7 功能有助于减少代码重复,并使你更容易指定要捕获的确切类型,而不是简单地捕获基本类型。你可以通过这种方式组合多种异常类型。

Java 标准异常

Throwable 这个 Java 类被用来表示任何可以作为异常被抛出的类。Throwable 对象可分为两种类型(指从 Throwable 继承而得到的类型):Error 用来表示编译时和系统错误(除特殊情况外,一般不用你关心);Exception 是可以被抛出的基本类型,在 Java 类库、用户方法以及运行时故障中都可能抛出 Exception 型异常。所以 Java 程序员关心的基类型通常是 Exception。要想对异常有全面的了解,最好去浏览一下 HTML 格式的 Java 文档(可以从 java.sun.com 下载)。为了对不同的异常有个感性的认识,这么做是值得的。但很快你就会发现,这些异常除了名称外其实都差不多。同时,Java 中异常的数目在持续增加,所以在书中简单罗列它们毫无意义。所使用的第三方类库也可能会有自己的异常。对异常来说,关键是理解概念以及如何使用。

异常的基本的概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。异常并非全是在 java.lang 包里定义的;有些异常是用来支持其他像 util、net 和 io 这样的程序包,这些异常可以通过它们的完整名称或者从它们的父类中看出端倪。比如,所有的输入/输出异常都是从 java.io.IOException 继承而来的。

特例:RuntimeException

image.png

如果必须对传递给方法的每个引用都检查其是否为 null(因为无法确定调用者是否传入了非法引用),这听起来着实吓人。幸运的是,这不必由你亲自来做,它属于 Java 的标准运行时检测的一部分。如果对 null 引用进行调用,Java 会自动抛出 NullPointerException 异常,所以上述代码是多余的。

属于运行时异常的类型有很多,它们会自动被 JVM 抛出,所以不必在异常说明中列出来。这些异常都是继承自 RuntimeException 。这构成了一组具有相同特征和行为的异常类型。并且,也不再需要在异常说明中声明方法将抛出 RuntimeException 类型的异常(或者任何从 RuntimeException 继承的异常),它们也被称为“不受检查异常”。这种异常属于错误,将被自动捕获,就不用你亲自动手了。要是自己去检查 RuntimeException 的话,代码就显得太混乱了。不过尽管通常不用捕获 RuntimeException 异常,但还是可以在代码中抛出 RuntimeException 类型的异常。

RuntimeException 代表的是编程错误:

  1. 无法预料的错误
    比如从你控制范围之外传递进来的 null 引用
  2. 作为程序员,应该在代码中进行检查错误
    比如 ArrayIndexOutOfBoundsException,就得注意一下数组大小。在一个地方发生的异常,常常会在另一个地方导致错误。

在这些情况下使用异常很有好处,它们能给调试带来便利。

如果不捕获这种异常会发生什么事?

因为编译器没有在这个问题上对异常说明进行强制检查,RuntimeException 类型的异常也许会穿越所有的执行路径直达 main() 方法,而不会被捕获。

要明白到底发生了什么,可以试试下面的例子:

image.png

输出结果为:

image.png

如果 RuntimeException 没有被捕获而直达 main(),那么在程序退出前将调用异常的 printStackTrace() 方法。

你会发现,RuntimeException 是个特例。对于这种异常,编译器无需异常说明,其输出被报告给了 System.err。

只能在代码中忽略 RuntimeException 类型的异常,因为所有受检查类型异常的处理都是由编译器强制实施的。

不应把 Java 的异常处理机制当成是单一用途的工具。它被设计用来处理一些烦人的运行时错误,这些错误往往是由代码控制能力之外的因素导致的;然而,它对于发现某些编译器无法检测到的编程错误,也是非常重要的。

使用 finally 进行清理

有一些代码片段,可能会希望无论 try 块中的异常是否抛出,它们都能得到执行。这通常适用于内存回收之外的情况(因为回收由垃圾回收器完成),为了达到这个效果,可以在异常处理程序后面加上 finally 子句。完整的异常处理程序看起来像这样:

image.png

为了证明 finally 子句总能运行,可以试试下面这个程序:

image.png

输出为:

image.png

可以从输出中发现,无论异常是否被抛出,finally 子句总能被执行。这个程序也给了我们一些思路,当 Java 中的异常不允许我们回到异常抛出的地点时,那么该如何应对呢?如果把 try 块放在循环里,就建立了一个“程序继续执行之前必须要达到”的条件。还可以加入一个 static 类型的计数器或者别的装置,使循环在放弃以前能尝试一定的次数。这将使程序的健壮性更上一个台阶。

finally 用来做什么?

对于没有垃圾回收和析构函数自动调用机制的语言来说,finally 非常重要。它能使程序员保证:无论 try 块里发生了什么,内存总能得到释放。但 Java 有垃圾回收机制,所以内存释放不再是问题。而且,Java 也没有析构函数可供调用。那么,Java 在什么情况下才能用到 finally 呢?

当要把除内存之外的资源恢复到它们的初始状态时,就要用到 finally 子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关,如下面例子所示:

image.png

输出为:

image.png

程序的目的是要确保 main() 结束的时候开关必须是关闭的,所以在每个 try 块和异常处理程序的末尾都加入了对 sw.offo 方法的调用。但也可能有这种情况:异常被抛出,但没被处理程序捕获,这时 sw.off() 就得不到调用。但是有了 finally,只要把 try 块中的清理代码移放在一处即可:

image.png

输出为:

image.png

程序的目的是要确保 main() 结束的时候开关必须是关闭的,所以在每个 try 块和异常处理程序的末尾都加入了对 sw.offo 方法的调用。但也可能有这种情况:异常被抛出,但没被处理程序捕获,这时 sw.off() 就得不到调用。但是有了 finally,只要把 try 块中的清理代码移放在一处即可:

image.png

输出为:

image.png

这里 sw.off() 被移到一处,并且保证在任何情况下都能得到执行。

甚至在异常没有被当前的异常处理程序捕获的情况下,异常处理机制也会在跳到更高一层的异常处理程序之前,执行 finally 子句:

image.png

输出为:

image.png

当涉及 break 和 continue 语句的时候,finally 子句也会得到执行。请注意,如果把 finally 子句和带标签的 break 及 continue 配合使用,在 Java 里就没必要使用 goto 语句了。

在 return 中使用 finally

因为 finally 子句总是会执行,所以可以从一个方法内的多个点返回,仍然能保证重要的清理工作会执行:

image.png

输出为:

image.png

从输出中可以看出,从何处返回无关紧要,finally 子句永远会执行。

缺憾:异常丢失

遗憾的是,Java 的异常实现也有瑕疵。异常作为程序出错的标志,决不应该被忽略,但它还是有可能被轻易地忽略。用某些特殊的方式使用 finally 子句,就会发生这种情况:

image.png

输出为

image.png

从输出中可以看到,VeryImportantException 不见了,它被 finally 子句里的 HoHumException 所取代。这是相当严重的缺陷,因为异常可能会以一种比前面例子所示更微妙和难以察党的方式完全丢失。相比之下,C++把“前一个异常还没处理就抛出下一个异常”的情形看成是糟糕的编程错误。也许在 Java 的未来版本中会修正这个问题(另一方面,要把所有抛出异常的方法,如上例中的 dispose() 方法,全部打包放到 try-catch 子句里面)。

一种更加简单的丢失异常的方式是从 finally 子句中返回:

image.png

如果运行这个程序,就会看到即使方法里抛出了异常,它也不会产生任何输出。

目录
相关文章
|
1天前
|
Java 开发者
深入理解Java中的异常处理机制
【9月更文挑战第6天】在Java编程的世界中,异常处理是一块不可或缺的拼图。就像我们在生活中遇到意外时需要冷静思考解决方案一样,Java程序也需要通过异常处理来应对运行时出现的问题。本文将引导你了解Java异常处理的核心概念,并教你如何巧妙地使用try-catch语句和finally块来捕获和处理异常。
11 2
|
9天前
|
消息中间件 算法 Java
深入浅出操作系统:进程管理的艺术掌握Java中的异常处理机制
【8月更文挑战第30天】在数字世界的舞台上,操作系统扮演着导演的角色,精心安排着每一个进程的表演。本文将揭开进程管理的神秘面纱,从进程的诞生到终结,探究它们如何在操作系统的指挥下和谐共舞。通过生动的比喻和直观的代码示例,我们将一同走进操作系统的核心,理解进程调度、同步与通信的内在机制,以及它们对计算生态的重要性。让我们跟随代码的节奏,一起感受操作系统的魅力吧!
|
5天前
|
Java C++
Java内存区域于内存溢出异常
这篇文章详细解释了Java虚拟机的内存区域划分、各区域的作用以及可能遇到的内存溢出异常情况。
14 0
|
5天前
|
安全 Java API
Java线程池原理与锁机制分析
综上所述,Java线程池和锁机制是并发编程中极其重要的两个部分。线程池主要用于管理线程的生命周期和执行并发任务,而锁机制则用于保障线程安全和防止数据的并发错误。它们深入地结合在一起,成为Java高效并发编程实践中的关键要素。
4 0
|
7天前
|
Java 开发者
Java编程中的异常处理机制探究
【8月更文挑战第31天】在Java的世界中,异常处理是维护程序稳定性的重要工具。它像是一套精密的免疫系统,保护代码免受错误的侵袭,确保程序能够优雅地应对意外情况。本文将带你走进Java的异常处理机制,了解如何捕获和处理异常,以及自定义异常类的创建与应用,让你的代码更加健壮,运行更加顺畅。
|
8天前
|
开发者 C# 自然语言处理
WPF开发者必读:掌握多语言应用程序开发秘籍,带你玩转WPF国际化支持!
【8月更文挑战第31天】随着全球化的加速,开发多语言应用程序成为趋势。WPF作为一种强大的图形界面技术,提供了优秀的国际化支持,包括资源文件存储、本地化处理及用户界面元素本地化。本文将介绍WPF国际化的实现方法,通过示例代码展示如何创建和绑定资源文件,并设置应用程序语言环境,帮助开发者轻松实现多语言应用开发,满足不同地区用户的需求。
18 0
|
8天前
|
Java 开发者
Java编程中的异常处理机制探究
【8月更文挑战第31天】 在Java的世界中,异常处理是维护程序稳定性的重要工具。它像是一套精密的免疫系统,保护代码免受错误的侵袭,确保程序能够优雅地应对意外情况。本文将带你走进Java的异常处理机制,了解如何捕获和处理异常,以及自定义异常类的创建与应用,让你的代码更加健壮,运行更加顺畅。
|
8天前
|
Java 数据库连接 API
Java中的异常处理:理解、实践与最佳实践
【8月更文挑战第31天】在Java编程的世界中,异常处理是保持代码健壮性的关键。本文将通过浅显易懂的方式,带你了解Java异常处理的基本概念,并通过具体示例展示如何有效管理异常。我们将一起学习如何使用try-catch语句来捕获和处理异常,以及finally块的重要性。同时,我们还将探讨一些最佳实践,帮助你编写更加稳定和可靠的Java代码。无论你是Java新手还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。
|
8天前
|
Java 程序员 开发者
深入理解Java中的异常处理机制
【8月更文挑战第31天】 本文旨在通过浅显易懂的方式,带你走进Java的异常世界。我们将从异常的基本概念出发,逐步深入到异常的分类、捕获和处理,最后通过代码示例来巩固你的理解。无论你是初学者还是有一定编程经验的开发者,这篇文章都将为你提供有价值的参考。
|
8天前
|
Java 程序员
深入浅出Java异常处理机制
【8月更文挑战第31天】本文旨在以浅显易懂的语言,为读者揭示Java异常处理的神秘面纱。通过日常生活中的小故事,我们将一步步走进异常的世界,探索它们的起源、分类以及如何妥善处理这些不请自来的特殊“客人”。文章中将穿插实际代码案例,帮助读者从理论到实践,全面掌握Java异常处理技巧。
下一篇
DDNS