有时候,您可能需要重新抛出捕获到的异常。这种情况通常发生在捕获到 Error 或 RuntimeException 时,因为您可能没有预料到这些异常,但在声明捕获 Throwable 和 Exception 时,它们也被包含在内了。为了解决这个问题,Guava 提供了多种方法来判断异常类型并重新抛出异常。例如:
try {
someMethodThatCouldThrowAnything();
} catch (IKnowWhatToDoWithThisException e) {
handle(e);
} catch (Throwable t) {
Throwables.propagateIfInstanceOf(t, IOException.class);
Throwables.propagateIfInstanceOf(t, SQLException.class);
throw Throwables.propagate(t);
}
所有这些方法都会自动决定是否需要抛出异常,但也可以直接抛出方法返回的结果。例如,使用 throw Throwables.propagate(t) 可以向编译器声明这里一定会抛出异常。
Guava 中的异常传播方法简要列举如下:
RuntimeException propagate(Throwable)
所以你可以像上面说的那样写成throw Throwables.propagate(t),Java 编译器会意识到这行代码保证抛出异常。
void propagateIfInstanceOf(Throwable, Class<X extends Exception>) throws X
方法为 void propagateIfInstanceOf(Throwable, Class),它的作用是只有当 Throwable 的类型为 X 时才抛出 X 异常。
void propagateIfPossible(Throwable)
只有当 Throwable 类型为 Error 或 RuntimeException 时才会抛出异常。
void propagateIfPossible( Throwable, Class<X extends Throwable>) throws X
只有当Throwable 类型为 X, Error 或 RuntimeException 才抛出
模仿 Java7 的多重异常捕获和再抛出
通常来说,如果调用者想让异常传播到栈顶,他不需要写任何 catch 代码块。因为他不打算从异常中恢复,他可
能就不应该记录异常,或者有其他的动作。他可能是想做一些清理工作,但通常来说,无论操作是否成功,清理
工作都要进行,所以清理工作可能会放在 finallly 代码块中。但有时候,捕获异常然后再抛出也是有用的:也许调
用者想要在异常传播之前统计失败的次数,或者有条件地传播异常。
当只对一种异常进行捕获和再抛出时,代码可能还是简单明了的。但当多种异常需要处理时,却可能变得一团
糟:
@Override
public void run() {
try {
delegate.run();
} catch (RuntimeException e) {
failures.increment();
throw e;
}catch (Error e) {
failures.increment();
throw e;
}
}
Java7 用多重捕获解决了这个问题:
} catch (RuntimeException | Error e) {
failures.increment();
throw e;
}
非Java7用户却受困于这个问题。他们想要写如下代码来统计所有异常,但是编译器不允许他们抛出 Throwabl
e。
} catch (Throwable t) {
failures.increment();
throw t;
}
解决办法是用 throw Throwables.propagate(t)替换 throw t。在限定情况下(捕获 Error 和 RuntimeExcepti
on),Throwables.propagate 和原始代码有相同行为。然而,用 Throwables.propagate 也很容易写出有其
他隐藏行为的代码。尤其要注意的是,这个方案只适用于处理 RuntimeException 或 Error。如果 catch 块捕获
了受检异常,你需要调用 propagateIfInstanceOf 来保留原始代码的行为,因为 Throwables.propagate 不能
直接传播受检异常。
总之,Throwables.propagate 的这种用法也就马马虎虎,在 Java7 中就没必要这样做了。在其他 Java 版本
中,它可以减少少量的代码重复,但简单地提取方法进行重构也能做到这一点。此外,使用 propagate 会意外地
包装受检异常。
有少数 API,尤其是 Java 反射 API 和(以此为基础的)JUnit,把方法声明成抛出 Throwable。和这样的 API
交互太痛苦了,因为即使是最通用的 API 通常也只是声明抛出 Exception。当确定代码会抛出 Throwable,而
不是 Exception 或 Error 时,调用者可能会用 Throwables.propagate 转化 Throwable。这里有个用 Callabl
e 执行 JUnit 测试的范例:
public Void call() throws Exception {
try {
FooTest.super.runTest();
} catch (Throwable t) {
Throwables.propagateIfPossible(t, Exception.class);
Throwables.propagate(t);
}
return null;
}
在这儿没必要调用 propagate()方法,因为 propagateIfPossible 传播了 Throwable 之外的所有异常类型,第
二行的 propagate 就变得完全等价于 throw new RuntimeException(t)。(这个例子也提醒我们,pr
opagateIfPossible可能也会引起混乱,因为它不但会传播参数中给定的异常类型,还抛出 Error 和 RuntimeE
xception)
这种模式(或类似于 throw new RuntimeException(t)的模式)在 Google 代码库中出现了超过 30 次。(搜索’propagateIfPossible[^;]* Exception.class[)];’)绝大多数情况下都明确用了”throw new RuntimeException(t)”。我们也曾想过有个”throwWrappingWeirdThrowable”方法处理 Throwable 到 Exception 的转化。但考虑到我们用两行代码实现了这个模式,除非我们也丢弃 propagateIfPossible 方法,不然定义这个 throwWrappingWeirdThrowable 方法也并没有太大必要。
在 Java 中,异常分为受检查异常和非受检查异常。受检查异常是指在方法声明中必须声明抛出的异常,而非受检查异常则是指在方法声明中不需要声明抛出的异常。受检查异常必须在方法内部进行处理,否则编译时会报错,而非受检查异常则可以不进行处理,但是如果不进行处理,程序会抛出运行时异常并终止执行。受检查异常一般是由 I/O 操作、网络操作等可能会出现异常的操作所引发的,而非受检查异常则是由程序错误或者逻辑错误所引发的。
原则上,非受检异常代表 bug,而受检异常表示不可控的问题。但在实际运用中,即使 JDK 也有所误用——如
Object.clone()、Integer. parseInt(String)、URI(String)——或者至少对某些方法来说,没有让每个人都信服
的答案,如 URI.create(String)的异常声明。
因此,调用者有时不得不把受检异常和非受检异常做相互转化:
try {
return Integer.parseInt(userInput);
} catch (NumberFormatException e) {
throw new InvalidInputException(e);
}
try {
return publicInterfaceMethod.invoke();
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
有时候,调用者会使用 Throwables.propagate 转化异常。这样做有没有什么缺点?最主要的恐怕是代码的含义
不太明显。
throw Throwables.propagate(ioException)做了什么?throw new RuntimeException(ioException)做了什么?这两者做了同样的事情,但后者的意思更简单直接。前者却引起了疑问:”它做了什么?它并不只是把异常包装进RuntimeException 吧?如果它真的只做了包装,为什么还非得要写个方法?”。应该承认,这些问题部分是因为”propagate”的语义太模糊了(用来抛出未声明的异常吗?)。
也许”wrapIfChecked”更能清楚地表达含义。但即使方法叫做”wrapIfChecked”,用它来包装一个已知类型的受检异常也没什么优点。甚至会有其他缺点:也许比起 RuntimeException,还有更合适的类型——如 IllegalArgumentException。 我们有时也会看到 propagate 被用于传播可能为受检的异常,结果是代码相比以前会稍微简短点,但也稍微有点不清晰:
} catch (RuntimeException e) {
throw e;
}catch (Exception e) {
throw new RuntimeException(e);
}
然而,我们似乎故意忽略了把检查型异常转化为非检查型异常的合理性。在某些场景中,这无疑是正确的做
法,但更多时候它被用于避免处理受检异常。这让我们的话题变成了争论受检异常是不是坏主意了,我不想对此
多做叙述。但可以这样说,Throwables.propagate 不是为了鼓励开发者忽略 IOException 这样的异常。
} catch (Exception e) {
throw Throwables.propagate(e);
}
如果你要实现不允许抛出异常的方法呢?有时候你需要把异常包装在非受检异常内。这种做法挺好,但我们再次强调,没必要用 propagate 方法做这种简单的包装。
实际上,手动包装可能更好:如果你手动包装了所有异常(而不仅仅是受检异常),那你就可以在另一端解包所有异常,并处理极少数特殊场景。此外,你可能还想把异常包装成特定的类型,而不是像 propagate 这样统一包装成 RuntimeException。
try {
return future.get();
} catch (ExecutionException e) {
throw Throwables.propagate(e.getCause());
}
对这样的代码要考虑很多方面:
ExecutionException 的 cause 可能是受检异常,见上文”争议一:把检查型异常转化为非检查型异
常”。但如果我们确定 future 对应的任务不会抛出受检异常呢?(可能 future 表示 runnable 任务的结
果)。
ExecutionException 的 cause 可能直接是 Throwable 类型,而不是 Exception 或 Error。(实际上这不
大可能,但你想直接重新抛出 cause 的话,编译器会强迫你考虑这种可能性)见上文”用法二:把抛出 Thr
owable 改为抛出 Exception”。
ExecutionException 的 cause 可能是非受检异常。如果是这样的话,cause 会直接被 Throwables.prop
agate 抛出。不幸的是,cause 的堆栈信息反映的是异常最初产生的线程,而不是传播异常的线程。通常来
说,最好在异常链中同时包含这两个线程的堆栈信息,就像 ExecutionException 所做的那样。(这个问题
并不单单和 propagate 方法相关;所有在其他线程中重新抛出异常的代码都需要考虑这点)。
Guava 提供了如下三个有用的方法,让研究异常的原因链变得稍微简便了,这三个方法的签名是不言自明的:
Throwable getRootCause(Throwable)
List<Throwable> getCausalChain(Throwable)
String getStackTraceAsString(Throwable)
【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实战指南(集合开发实战篇)