Java异常以及处理原则

Java异常以及处理原则

好的程序设计语言能帮助程序员写出好程序,但无论哪种语言都避免不了程序员用它写出了坏程序。——Bertrand Meyer

原则

Java的基本理念是“结构不佳的代码不能运行”。

发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前。然而,编译期并不能找出所有的错误,余下的问题必须在运行期间解决。这就需要错误源能通过某种方式,把适当的信息传递给某个接收者——该接收者将知道如何正确的处理这个问题。

改进的错误恢复机制是提供代码健壮性的最强有力的方式。错误恢复在我们所编写的每一个程序中都是基本的要素,但是在Java中它显得格外重要,因为Java的主要目标之一就是创建他人使用的程序构件。要想创建健壮的系统,它的每一个构件都必须是健壮的。Java使用异常来提供一致的错误报告模型,使得构件能够与客户端代码可靠地沟通问题。

Java中的异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加自信:你的应用中没有未处理的错误。

故障的代价

如果应用程序的异常处理方式既不连续又不通用会出现以下问题。

  • 扩展性和维护性不高:如果不采用标准处理模型,系统将难以修改。
  • 级联故障:如果对错误缺乏全局的直观认识,那么系统某一部分的错误可能扩展到整个应用程序。甚至可能导致一个或多个服务器崩溃,降低应用程序的可用性。
  • 处理不一致:如果缺少一个全局化组织模型,在处理异常时,开发人员可能作出与本团队其他人员冲突的决策。从而降低系统的整体有效性和可管理性。
  • 降低系统性能:如果处理不当,将引发无谓的系统调用和数据传输。从而降低系统的总体性能。
  • 丢失或损坏数据:局部化异常处理的副作用在于:通常很难与应用程序其他部分协调。这增加了问题的风险程度,从而损坏了业务模型,危及业务数据的完整性。

历史

异常处理起源于PL/1和Mesa之类的系统中,后来又出现在CLU、Smalltalk、Modula-3、Ada、Eiffel、C++、Python、Java以及后Java语言Ruby和C#中。Java的设计和C++很相似,只是Java的设计者去掉了一些他们认为C++设计得不好的东西。

为了能像程序员提供一个他们更愿意使用的错误处理和恢复的框架,异常处理机制很晚才被加入C++标准化过程中,这是由C++的设计者Bjarne Stroustrup所倡议的。C++的异常模型主要借鉴了CLU的做法。然而,当时其他语言已经支持异常处理了:包括Ada、Smalltalk(两者都有异常处理,但是都没有异常说明),以及Modula-3(它既有异常处理也有异常说明)。

Liskov和Snyder在它们的一篇讨论该主题的独创性论文1中指出,用瞬时风格(transient fashion)报告错误的语言(如C中)有一个主要缺陷,那就是:

……每次调用的时候都必须执行条件测试,以确定会产生何种结果。这使程序难以阅读,并且有可能降低运行效率,因此程序员既不愿指出,也不愿意处理异常。

C++中异常的设计参考了CLU方式。Stroustrup声称其目标是减少恢复错误所需要的代码。我想他这话是说给那些通常情况下都不写C的错误处理的程序员们听的,因为要把那么多代码放到那么多地方实在不是什么好差事。所以他们写C程序的习惯是,忽略所有的错误,然后使用调试器来跟踪错误。这些程序员知道,使用异常就意味着他们要写一些通常不用写的、“多出来的”代码。因此,要把他们拉到“使用错误处理”的正规上,“多出来的”代码决不能太多。

C++从CLU那里还带来另一种思想:异常说明。这样,就可以用编程的方式在方法的特征签名中,声明这个方法将会抛出异常。异常说明可能有两种意思。一个是“我的代码会产生这种异常,这由你来处理”。另一个是“我的代码忽略了这些异常,这由你来处理”。

值得注意的是,由于使用了模板,C++标准类库实现里根本没有使用异常说明。在Java中,对于泛型用于异常说明的方式存在着一些限制。

体系

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。

Java异常体系结构呈树状,其层次结构图如图所示: 
《Java异常以及处理原则》

Thorwable类是所有异常和错误的超类,有两个子类Error和Exception,分别表示错误和异常。 其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

Error与Exception

Error是程序无法处理的错误,比如OutOfMemoryErrorThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

Error表示程序在运行期间出现了十分严重、不可恢复的错误,在这种情况下应用程序只能中止运行,例如Java虚拟机出现错误。Error是一种unchecked Exception,编译器不会检查Error是否被处理,在程序中不用捕获Error类型的异常。一般情况下,在程序中也不应该抛出Error类型的异常。

运行时异常和非运行时异常

运行时异常都是RuntimeException类及其子类异常,`NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。

这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 RuntimeException发生的时候,应该通过改进程序来避免这类异常,而不是去捕获RuntimeException

所有继承自Exception并且不是RuntimeException的异常都是checked Exception,上图中的IOExceptionClassNotFoundException。JAVA 语言规定必须对checked Exception作处理,编译器会对此作检查,要么在方法体中声明抛出checked Exception,要么使用catch语句捕获checked Exception进行处理,不然不能通过编译。

内置异常类

Java 语言定义了一些异常类在java.lang标准包中。

标准运行时异常类的子类是最常见的异常类。由于java.lang包是默认加载到所有的Java程序的,所以大部分从运行时异常类继承而来的异常都可以直接使用。

Java根据各个类库也定义了一些其他的异常,下面的表中列出了Java的非检查性异常。

异常描述
ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数”除以零”时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException抛出的异常表明向方法传递了一个不合法或不正确的参数。
IllegalMonitorStateException抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。
IllegalStateException在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。
IllegalThreadStateException线程没有处于请求操作所要求的适当状态时抛出的异常。
IndexOutOfBoundsException指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
NegativeArraySizeException如果应用程序试图创建大小为负的数组,则抛出该异常。
NullPointerException当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
SecurityException由安全管理器抛出的异常,指示存在安全侵犯。
StringIndexOutOfBoundsException此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。
UnsupportedOperationException当不支持请求的操作时,抛出该异常。

下面的表中列出了Java定义在java.lang包中的检查性异常类。

异常描述
ClassNotFoundException应用程序试图加载类时,找不到相应的类,抛出该异常。
CloneNotSupportedException当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。
IllegalAccessException拒绝访问一个类的时候,抛出该异常。
InstantiationException当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。
InterruptedException一个线程被另一个线程中断,抛出该异常。
NoSuchFieldException请求的变量不存在
NoSuchMethodException请求的方法不存在

优缺点

终止与恢复

异常处理理论上有两种基本模型。Java支持终止模型(这与大多数语言的机制相同,包括C++、C#、Python等)。在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。

另一种称为恢复模型。意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。

如果想要用Java实现类似的恢复的行为,那么在遇见错误时就不能抛出异常,而是调用方法来修正该错误。或者,把try块放在while循环里,这样就不断地进入try块,直到得到满意的结果。

长久以来,尽管程序员们使用的操作系统支持恢复模型的异常处理,但他们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是他所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。

我一直怀疑到底有多少时候“恢复”真正得以实现,或者能够实现。我认为这种情况少于10%,并且即便是这10%,也只是将栈展开到某个已知的稳定状态,而并没有实际执行任何种类的恢复性行为。Java坚定地强调将所有的错误都以异常形式报告的这一事实,正是它远远超过诸如C++这类语言的长处之一,因为在C++这类语言中,需要以大量不同的方式来报告错误,或者根本就没有提供错误报告功能。

异常丢失

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

class VeryImportandException extends Exception { public String toString() { return "A very important exception"; } } class HoHumException extends Exception { public String toString() { return "A trivial exception"; } } public class LostMessage { void f() throws VeryImportandException { throw new VeryImportantException(); } void dispose() throws HoHumException { throw new HoHumException(); } public static void main(String[] args) { try { LostMessage lm = new LostMessage();\ try { lm.f(); } finally { lm.dispose(); } } catch (Exception e) { System.out.println(e); } } } /* Output A trivial exception */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

从输出中可以看到,VeryImportantException不见了,它被finally子句里的HoHumException所取代。这是相当严重的缺陷。因为异常可能会以一种比前面例子所示更微妙和难以察觉的方式完全丢失。

相比之下,C++把“前一个异常还没处理就抛出下一个异常”的情形看成是糟糕的编程错误。

构造器

构造过程中的异常

有一点很重要,即你要时刻询问自己“如果异常发生了,所有的东西能被正确的清理吗?”尽管大多数情况下是非常安全的,但涉及构造器时,问题就出现了。构造器会把对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得到清理。如果在构造器内抛出异常,这些清理行为也许就不能正常工作了。这意味着在编写构造器时要格外细心。

读者也许会认为使用finally就可以解决问题。但问题并非如此简单,因为finally会每次都执行清理代码。如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在finally子句中却要被清理的。

public class InputFile { private BufferedReader in; public InputFile (String fname) throws Exception { try { in = new BufferedReader (new FileReader(fname)); // Ohter code that might throw exceptions; } catch (FileNotFoundException e) { // Wasn't open, so don't close it throw e; } catch (Exception e) { // All other exceptions must close it try { in.close(); } catch (IOException e2) { System.out.println("in.close() unsuccessful") } throw e; // Rethrow } finally { // Don't close it here } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

如果FileReader的构造器失败了,将抛出FileNotFoundException异常。对于这个异常,并不需要关闭文件,因为这个文件还没有被打开。而任何其他捕获异常的catch子句必须关闭文件,因为在它们捕获到异常之时,文件已经打开了。异常被重新抛出非常重要,因为如果不如此的话会误导调用方,让他认为“这个对象已经创建完毕,可以使用了”。

异常的限制

当覆蓋方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,当基类使用的代码应用到其派生类对象的时候,一样能够工作,异常也不例外。

然而,异常限制对构造器不起作用。你会发现派生类的构造器可以抛出任何异常,而不必理会基类构造器所抛出的异常。由于这样或那样调用方式,派生类构造器的异常说明必须包含基类构造器的异常说明。

派生类构造器不能捕获基类构造器抛出的异常。

被检查的异常

这个话题看起来简单,但实际上它不仅复杂,更重要的是还非常多变。总有人会顽固的坚持自己的立场,声称正确答案(也是他们的答案)是显而易见的。我觉得之所以会有这种观点,是因为我们使用的工具已经不是ANSI标准出台前的像C那样的弱类型语言,而是像C++和Java这样的“强静态类型语言”(也就是编译时就做类型检查的语言),这是前者所无法比拟的。当刚开始这种转变的时候(就像我一样),会觉得它带来的好处是那样明显,好像类型检查总能解决所有的问题。在此,结合我自己的认识过程,简述我是怎样从对类型检查的绝对迷信变成持怀疑态度的;当然,很多时候它还是非常有用的,但是当它挡住我们的去路并成为障碍的时候,我们就得跨过去。只是这条界限往往并不是很清晰。

首先,Java无谓地发明了“被检查的异常”(很明显是受C++异常说明的启发,以及受C++程序员们一般对此无动于衷的事实的影响)。目前为止还没有别的语言采用这种做法。

其次,仅从示意性的例子和小程序来看,“被检查的异常”的好处很明显。但是当程序开始变大的时候,就会带来一些微妙的问题。当然,程序不是一下就变大的,这有个过程。如果把不适用于大项目的语言用于小项目,当这些项目不断膨胀时,突然有一天你会发现,原来可以管理的东西,现在已经变得无法管理了。这就是我所说的过多的类型检查,特别是“被检查的异常”所造成的问题。

看来程序的规模是个重要因素。由于很多讨论都用小程序来做演示,因此这并不足以说明问题。一名C#的设计人员发现:

仅从小程序来看,会认为异常说明能增加开发人员的效率,并提高代码的质量;但考察大项目的时候,结论就不同了——开发效率下降了,而代码质量只有微不足道的提高,甚至毫无提高。

在解释为什么“函数没有异常说明就表示可以抛出任何异常”的时候,Stroustrup这样认为:

但是,这样一来几乎所有的函数都得提供异常说明了,也就是都得重新编译,而且还会妨碍它同其他语言的交互。这样会迫使程序员违反异常处理机制的约束,他们会写欺骗程序来掩盖异常。这将给没有注意到这些异常的人造成一种虚假的安全感。2

我们已经看到这种破坏异常机制的行为了——就在Java的“被检查的异常”里。

Martin Fowler(UML Distilled, Refactoring和Analysis Patterns的作者)也说过:

……总体来说,我觉得异常不错,但是Java的“被检查异常”带来的麻烦比好处多。

过去,我曾坚定的认为“被检查的异常”和强静态类型检查对开发健壮的程序是非常必要的。但是,我看到的以及我使用一些动态语言(Python)的亲身经历告诉我,这些好处实际上是来自于:

  1. 不在于编译器是否会强制程序员去处理错误,而是要有一致的、使用异常来报告错误的模型。
  2. 不在于什么时候进行检查,而是一定要有类型检查。

此外,减少编译时施加的约束能显著提高程序员的编程效率。事实上,反射泛型就是用来补偿静态类型检查所带来的过多限制。

处理原则

异常类型的选择

按照异常的应用场景,可以概括为:

  1. 调用代码不能继续执行,需要立即终止。出现这种情况的可能性太多太多,例如服务器连接不上、参数不正确等。这些时候都适用非检测异常,不需要调用代码的显式捕捉和处理,而且代码简洁明了。
  2. 调用代码需要进一步处理和恢复。假如将 SQLException 定义为非检测异常,这样操作数据时开发人员理所当然的认为 SQLException 不需要调用代码的显式捕捉和处理,进而会导致严重的 Connection 不关闭、Transaction 不回滚、DB 中出现脏数据等情况,正因为 SQLException 定义为检测异常,才会驱使开发人员去显式捕捉,并且在代码产生异常后清理资源。当然清理资源后,可以继续抛出非检测异常,阻止程序的执行。根据观察和理解,检测异常大多可以应用于工具类中。

异常的转译

所谓的异常转译就是将一种异常转换另一种新的异常,也许这种新的异常更能准确表达程序发生异常。

在Java中有个概念就是异常原因,异常原因导致当前抛出异常的那个异常对象,几乎所有带异常原因的异常构造方法都使用Throwable类型做参数,这也就为异常的转译提供了直接的支持,因为任何形式的异常和错误都是Throwable的子类。比如将SQLException转换为另外一个新的异常ServiceException

异常转译是针对所有继承Throwable超类的类而言的,从编程的语法角度讲,其子类之间都可以相互转换。但是,从合理性和系统设计角度考虑,可将异常分为三类:ErrorExceptionRuntimeException,笔者认为,合理的转译关系图应该如图所示: 
《Java异常以及处理原则》

最佳实践

有关异常框架设计这方面公认比较好的就是spring,Spring中的所有异常都可以用org.springframework.core.NestedRuntimeException来表示,并且该基类继承的是RuntimeException。Spring框架很庞大,因此设计了很多NestedRuntimeException的子类,还有异常转换的工具,这些都是非常优秀的设计思想。

    OutputStreamWriter out = null; java.sql.Connection conn = null; try { // ---------1 Statement stat = conn.createStatement(); ResultSet rs = stat.executeQuery("select *from user"); while (rs.next()){ out.println("name:" + rs.getString("name") + "sex:" + rs.getString("sex")); } conn.close(); //------2 out.close(); } catch (Exception ex){ //------3 ex.printStackTrace(); //------4 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

尽可能减小try块

问题1

对于这个try…catch块,真正目的是捕获SQL的异常,但是这个try块是不是包含了太多的信息了。这是我们为了偷懒而养成的代码坏习惯。有些人喜欢将一大块的代码全部包含在一个try块里面,因为这样省事,反正有异常它就会抛出,而不愿意花时间来分析这个大代码块有那几块会产生异常,产生什么类型的异常,反正就是一篓子全部搞定。这就想我们出去旅游将所有的东西全部装进一个箱子里面,而不是分类来装,虽然在捕获的时候工作减轻了,但是在找回异常的时候会大大增加工作量。所有对于一个异常块,我们应该仔细分清楚每块的抛出异常,因为一个大代码块有太多的地方会出现异常了。

使用try资源语句和finally

问题2

在这里你发现异常改变了运行流程。如果该程序发生了异常那么conn.close(); out.close();是不可能执行得到的,这样势必会导致资源不能释放掉。所以如果程序用到了文件、Socket、JDBC连接之类的资源,即使遇到了异常,我们也要确保能够正确释放占用的资源。

不要使用覆蓋式异常处理

问题3

在多层次中这样捕捉,会丢失原始异常的有效信息。或者如以下更明显的代码所示

    public void retrieveObjectById(Long id){ try{ //…抛出 IOException 的代码调用 //…抛出 SQLException 的代码调用 }catch(Exception e){ //这里利用基类 Exception 捕捉的所有潜在的异常,如果多个层次这样捕捉,会丢失原始异常的有效信息 throw new RuntimeException(“Exception in retieveObjectById”, e); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

使用覆蓋式异常处理块有两个前提之一: 
1. 代码中只有一类问题。 
这可能正确,但即便如此,也不应使用覆蓋式异常处理,捕获更具体的异常形式有利无弊。 
2. 单个恢复操作始终适用。 
这几乎绝对错误。几乎没有哪个方法能放之四海而皆准,能应对出现的任何问题。 
分析下这样编写代码将发生的情况。只要方法不断抛出预期的异常集,则一切正常。但是,如果抛出了未预料到的异常,则无法看到要采取的操作。当覆蓋式处理器对新异常类执行千篇一律的任务时,只能间接看到异常的处理结果。如果代码没有打印或记录语句,则根本看不到结果。 
更糟糕的是,当代码发生变化时,覆蓋式处理器将继续作用于所有新异常类型,并以相同方式处理所有类型。

异常被捕获的选择

问题4

打印出异常信息在调试阶段有用,但是在运行阶段,仅仅打印没有产生任何有益的行为,并不能算作被处理。

  1. 处理异常。对所发生的的异常进行一番处理,如修正错误、提醒。
  2. 重新抛出异常。
  3. 封装异常。这是最好的处理方法,对异常信息进行分类,然后进行封装处理。
  4. 不要捕获异常。

避免在循环中捕获异常

    for(int i=0; i<100; i++){ try{ }catch(XXXException e){ //…. } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

异常处理占用系统资源。

多层次打印异常

    public class A { private static Logger logger = LoggerFactory.getLogger(A.class); public void process(){ try{ //实例化 B 类,可以换成其它注入等方式 B b = new B(); b.process(); //other code might cause exception } catch(XXXException e){ //如果 B 类 process 方法抛出异常,异常会在 B 类中被打印,在这里也会被打印,从而会打印 2 次 logger.error(e); throw new RuntimeException(/* 错误代码 */ errorCode, /*异常信息*/msg, e); } } } public class B{ private static Logger logger = LoggerFactory.getLogger(B.class); public void process(){ try{ //可能抛出异常的代码 } catch(XXXException e){ logger.error(e); throw new RuntimeException(/* 错误代码 */ errorCode, /*异常信息*/msg, e); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

同一段异常会被打印 2 次。如果层次再复杂一点,不去考虑打印日志消耗的系统性能,仅仅在异常日志中去定位异常具体的问题已经够头疼的了。 
其实打印日志只需要在代码的最外层捕捉打印就可以了,异常打印也可以写成 AOP(Aspect Oriented Programming),织入到框架的最外层。

优先使用标准异常

专家级程序员与缺乏经验的程序员一个最主要的区别在于,专家追求并且通常也能够实现高度的代码重用。代码重用是值得提倡的,这是一条通用的规则,异常也不例外。

重用现有的异常有多方面的好处。其中最主要的好处是,它使你的API更加易于学习和使用,因为它与程序员已经熟悉的习惯用法是一致的。第二个好处是,对于用到这些API的程序而言,他们的可读性会更好,因为他们不会出现很多程序员不熟悉的异常。最后(也是最不重要的)一点是,异常类越少,意味着内存印迹(footprint)就越小,装载这些类的时间开销也越少。

多线程与异常

不讨论线程,异常处理的介绍就不完整。在Java语言创立之初,线程就是一项非常重要的功能。它允许应用程序定义多个可并行执行的操作路径。线程具有很多重要的优点。它允许你管理程序中不受限制的独立任务,还允许共享正在运行的应用程序的内存,更有效的使用计算机的可用CPU资源。

但线程也有一些缺点。为了解决与数据冲突和损坏相关的一系列问题,编程时要格外小心,还要认真处理多线程应用程序中的异常处理。在单线程程序中,忽略一个问题可能导致应用程序崩溃。而在多线程程序中,如果忽略异常,则应用程序可能继续运行,而问题将可能从一个线程传到另一个线程。

在一个多线程Java应用程序中,除主应用程序线程外,每个线程都必须符合两项关键要求:

  • 由Thread对象管理
  • 与实现Runnable接口的类关联

两种情况都需要定义run方法中运行的线程代码。run方法的签名如下:

public void run()
  • 1
  • 1

可以看到,不能从一个独立线程中抛出任何受检异常,你必须在run方法内部处理它们。这样做的缺点是:你必须在代码中处理更大的异常集。其优点是:可以确保异常不关闭现在正在运行的线程。

当run方法抛出非受检异常时,负责该执行路径的Thread将终止。如果应用程序中有其他活动的线程,那些线程将继续运行,仿佛什么都没有发生。

需要自我确定在单个线程中抛出非受检异常时是否要停止整个应用程序。单个线程的问题并不一定使整个应用程序崩溃。但若不加处理,一个线程的问题可能产生“波纹”效应,引发其他线程发生问题。

如果应用程序出现严重问题,则可能要采取步骤来关闭所有线程。可采用“强力”解决方案(如使用System.exit),但更常用的办法是分别关闭各个线程,以免损坏或丢失数据。

这就是说,要认真分析代码中的异常处理,也要注意非受检异常。最理想的情况是,如果线程互相依赖,则多线程系统不抛出异常(受检异常和非受检异常)。线程活动的风险非常高。

说明

本文大量内容来自于《Java编程思想》、《Effective Java》、《Robust Java》、《Java语言规范》以及一些博客内容。一并致谢。

  1. Exception Handling in CLU, IEEE Transactions on Software Engineering, Vol. SE-5, No. 6, 1979年11月 
  2. Bjarne Struopstrup, The C++ Programming Language, 3rd edition, Addison-Wesley 1997, page 376 
    原文作者:天涯海角路
    原文地址: https://www.cnblogs.com/aademeng/articles/6140424.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞