Java进阶之详谈Exception

2,391 阅读9分钟

写在最前

最近笔者在撰写JavaWeb与自动化相结合的教程,上篇入口在这里,第二篇还在创作中,在发布之前,让我们先来讨论一个Java的重要技能,Exception。

实现程序的运行是所有初级的程序员所追求的,Thinking in Java 因此成为了很适合入门的一本书,然而随着代码行数的累积,越来越多的坑也随之到来。此时,对基础知识更深层次的理解就尤为关键。在JavaWeb与自动化结合的应用中,无脑抛出异常会导致代码的冗余与羸弱,今天发的这篇文章将仔细地对Exception的运用进行分析。

需要注意的是,本篇文章并不是对如何抛出异常的基础进行讲解,需要读者对Exception机制有一定了解,文中部分用例来自Effective Java,在这里同时向读者推荐这本书作为Java进阶的重要工具,文末附录中有笔者Exception部分的英文笔记供大家参考。

使用Exception的情景

不要在类似迭代的循环中使用Exception,尤其是涉及ArrayIndexOutOfBounds,如下所示:

try {
    int i = 0;
    while(true)
        array[i++].doSomething();
} catch(ArrayIndexOUtOfBoundsException e) {

}

主要因为此时使用try-catch有三点显而易见的坏处:

  • 这样做违背于JVM设置exception处理的原则,JVM会花费更多的时间来处理。
  • 把Code放在try-catch语句中使得一些JVM运行中的优化被封禁。
  • 规范的迭代写法是经过优化的,通过JVM的内部处理,避免了很多赘余的检查机制,是更合适的选择。

如果我们在try-catch语句中调用了另一个数组,这个数组中出现了ArrayIndexOutOfBounds的异常,其中的bug就会被catch exception所蒙蔽。相反,标准的迭代写法会及时的终止线程的执行,报出错误并且给出追踪错误的路径让程序员更轻松地定位bug的来源。

现在我们通过Java的Iterator接口来看一下标准迭代写法,在标准的迭代写法中,我们利用hasNext()作为state-testing判断方法,来实现state-dependent方法next(),代码如下:

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
    //...
}

综上所述,Exception是为了异常或者说例外的情况而准备的,不应该在普通的语句中使用,并且程序员也不该写出强迫他人在正常流程的语句中使用Exception的API。

Checked与Unchecked的区别

在Java中Throwable是Exception与Error的父类,而在Effective Java书中,Throwable被分为了以下三类:

  1. Checked Exceptions
  2. Runtime Exceptions
  3. Errors

其中,2和3都是Unchecked Throwable,所以在我们分析Java的异常类时,从Checked与Unchecked两个逻辑角度来分析会更加清晰。

Checked Exception指那些在编译过程中会检查的,这类错误在运行中是“可恢复的”。我们需要在写程序时将其抛出,换而言之,这些异常应该并不是由程序员所导致,而是类似”例行检查“。

相反,Runtime Exception指的就是程序员本身制造出来的错误,在文章的第一部分中我们已经明确指出,此类错误不应该被抛出,而应该由程序员自己去修复。需要注意的是,一般来说,我们自己设计的Exception应该作为Runtime Exception的直接或者间接子类。如果你对Exception理解得比较浅,暴力地把Runtime Exception的子类背下来,对debug的帮助也相当大,可以快速定位代码中的问题。

Errors与Exception不同,他是与JVM相关的,当你在写算法时看到栈溢出,那并不是你对语言的理解导致你的代码出现漏洞,而是你的数据结构使得JVM出现resource deficiency或invariant failures使得程序无法继续执行,所以看到Errors的时候,我们也不应将其抛出,而是应该对代码结构进行修改处理。

综上所述,如果在运行中可恢复,那么我们就应该将这种Checked Exception抛出。当不清楚该如何做的时候,抛出Runtime Exception。重要的是,不要定义既不是Checked Exception子类也不是Runtime Exception子类的Throwable,并且记得在你自定义的Checked Exception中加入方法使代码能在运行中恢复。

Checked Exception的使用技巧

我们经常会遇到这种问题,在一个方法中,有一行代码需要抛出Exception,我们需要将他包裹在try-catch语句中。在Java8之后,我们在使用此API时必须抛出这个异常,这极大地降低了我们代码的质量。

解决这个问题最简单的方法可能就是我们在运行此方法是不返回任何值,但是如果这样做我们就少了很多通过此方法返回信息和数据的机会。

因此我们提供了另一种解决方式,那便是通过将需要抛出Checked Exception的方法拆为两个方法,使其转变为一个Unchecked Exception。第一个方法通过返回一个boolean值来指明此Exception是否应该被抛出,第二个再进行剩余的操作。下面是一个转变的简单例子。

包裹在try-catch中的语句:

try {
    ted.read(book);
} catch (CheckedException e) {
    //...do sth.
}

下面是改造后的代码:

if (ted.understand(book)) {
    ted.read(book);
} else {
    //...do sth.
}

简单来说,就是本来是再Ted”读“这个方法中抛出他看不懂这个书的异常,但我们将其拆分为”是否理解“与“读”两个方法对其进行重构,来避免try-catch的运用。

总的来说,重构Checked Exception是为了代码更简洁更可靠,避免了对Checked Exception的过度使用,因为过度使用会导致API对使用者很不友好。在遇到上面所说的情况时,首先考虑能否使用返回值为空的方法,因为这是最直接最简单的解决方式。

优先使用标准库中的Exception

使用Java库中提供地Exception有三大好处:

  1. 使你的API更容易地被学习与使用,因为大多数程序员都了解标准的异常
  2. 让使用了你的API的程序阅读起来更轻松
  3. 更少地占用内存并且更快地对Class进行加载(JVM)

不要直接重用Exception, RuntimeException, Throwable或是Error这些父类,常用的Exception在下表中列出。

Exception 使用场景
IllegalArgumentException 不匹配的非空参数的传递
IllegalStateException 未初始化的对象(对象状态不匹配)
NullPointerException 在未预期的情况下遭遇空指针
IndexOutOfBoundsException 索引参数超出范围
ConcurrentModificationException 多线程对同一个对象进行修改
UnsupportedOperationException 此对象不支持对此方法的引用

需要注意的是,重用的Exception一定要与记录的语义一致,在文档中详细说明,并不只是简单地匹配Exception的名字。

结语

除了上面详述的几点外,还要注意的是,首先,每个方法抛出的异常都要有文档。其次,保持异常的原子性。最重要的是,千万不要在catch中忽略掉捕获到的异常

关于异常处理对于很多人来说只是Alt+Enter,但是在代码优化阶段经常很让人头疼,希望本文能使大家有所启发,对于接下来教程中的一些代码有更好的理解,也欢迎大家提问,共同提高。

附录:Effective Java 读书笔记

Chapter 10 EXCEPTIONS

Item 69: Use exceptions only for exceptional conditions

Do not use try catch to handle your loop, it might mask the bug and is also very slow.

Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow and do not write APIs that force others to do so.

A well designed API must not force its clients to use exceptions for ordinary control flow.

In iteration codes, one should use hasNext() to decide the life circle of a loop.

Item 70: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

Use checked exceptions for conditions from which the caller can reasonably be expected to recover.

Use runtime exceptions to indicate programming errors.

All of the unchecked throwables you implement should subclass RuntimeException (directly or indirectly).

Don't define any throwables that are neither checked exceptions nor runtime exceptions.

Provide methods on your checked exceptions to aid in recovery.

Item 71: Avoid unnecessary use of checked exceptions

In Java 8, methods throwing checked exceptions can't be used directly in streams.

How to solve the problem that if a method throws a single checked exception, this exception is the sole reason the method must appear in a try block and can't be used directly in streams?

The easiest way to eliminate this is to return an optional of the desired result type.

You can also turn a checked exception into an unchecked exception by breaking the method that throws the exception into two methods, the first of which returns a boolean indicating whether the exception would be thrown.

Item 72: Favor the use of standard exceptions

The Java libraries provide a set of exceptions that covers most of the exceptions-throwing needs of most APIs.

Benefits: makes your API easier to learn because it matches the established conventions, makes programs using your API easier to read, a smaller memory footprint and less time spent loading classes.

Do not reuse Exception, RuntimeException, Throwable, or Error directly.

Reuse must be based on documented semantics, not just on name.

Item 73: Throw exceptions appropriate to the abstraction

Higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction, aka. Exception Translation.

While exception translation is superior to mindless propagation of exceptions from lower layers, it should not be overused.

If it is not feasible to prevent or to handle exceptions from lower layers, use exception translation, unless the lower-level method happens to guarantee that all of its exceptions are appropriate to the higher level.

Item 74: Document all exceptions thrown by each method

Always declare checked exceptions individually, and dovument precisely the conditions under which each one is thrown using @throws tag.

Use the Javadoc @throws tag to document each exception that a method can throw, but do not use the throws keyword on unchecked exceptions.

If an exception is thrown by many methods in a class for the same reason, you can document the exception in the class's documentation comment.

Item 75: Include failure-capture information in detail messages

To capture a failure, the detail message of an exception should contain the values of all parameters and fields that contributed to the exception.

Do not include passwords, encryption keys, and the like in detail messages.

Item 76: Strive for failure atomicity

A failed method invocation should leave the object in the state that it was in prior to the invocation.

Item 77: Don't ignore exceptions

An empty catch block defeats the purpose of exceptions.

If you choose to ignore an exception, the catch block should contain a comment explaining why it is appropriate to do so, and the variable should be named ignored.