在Java中如何捕获通用异常?

42

我们在工作中使用JUnit 3,没有ExpectedException注释。我想添加一个实用程序到我们的代码中来处理此问题:

 try {
     someCode();
     fail("some error message");
 } catch (SomeSpecificExceptionType ex) {
 }

所以我尝试了这个:

public static class ExpectedExceptionUtility {
  public static <T extends Exception> void checkForExpectedException(String message, ExpectedExceptionBlock<T> block) {
     try {
        block.exceptionThrowingCode();
        fail(message);
    } catch (T ex) {
    }
  }
}

然而,在Java的catch块中无法使用泛型异常类型。

那么,如何绕过Java的限制实现类似的功能呢?

是否有一种方法可以检查ex变量是否为T类型?


看起来你是对的,它无法捕获泛型。很遗憾。https://dev59.com/W3E95IYBdhLWcg3wHqWl - rogerdpack
该死的Java。这只是在尝试使用Java 8的lambda使代码稍微清晰一点时,你在我们的道路上抛出的众多障碍之一。 - Kenogu Labz
7个回答

37
你可以传递 Class 对象并进行编程检查。
public static <T extends Exception> void checkForException(String message, 
        Class<T> exceptionType, ExpectedExceptionBlock<T> block) {
    try {
       block.exceptionThrowingCode();
   } catch (Exception ex) {
       if ( exceptionType.isInstance(ex) ) {
           return;
       } else {
          throw ex;  //optional?
       }
   }
   fail(message);
}

//...
checkForException("Expected an NPE", NullPointerException.class, //...

我不确定你是否需要重新抛出异常,抛出异常会使测试同样失败/出错,但从语义上讲,我认为不应该这样做,因为它基本上意味着“我们没有得到预期的异常”,因此代表了编程错误,而不是测试环境错误。


你应该包含一个完整的使用示例,以便人们可以看到它与标准习惯用法一样简单易懂。 - Kevin Bourrillion
@Pete Kirkham:isAssignableFrom以一个类作为参数,本质上与isInstance在对象上所做的事情相同。如果参数与类兼容,则isInstance返回true,因此它同样适用于子类。 - Mark Peters
@Kevin Bourillion:我更关心技术上的可能性,而不是倡导一种方式。我认为你的帖子正确地指出了这种方法的危险,并已经相应地给你点赞了。 - Mark Peters
虽然我同意@KevinBourrillion的观点,但我真的不喜欢每次都剪切和粘贴完全相同的try catch处理程序代码来处理异常。我想出了一个非常类似于这个的解决方案,除此之外,我还会对错误消息进行断言,以便我知道到底是哪个具体的错误被抛出了。我可以接受稍微降低清晰度的代价,来使用一个标准化的模式,这样易于识别,而且编码起来更加美好。 - Marquee
也许可以使用throw new RuntimeException(ex),对于那些无法抛出Exception的人来说。 - rogerdpack

4
我理解你想要简化异常测试习惯的冲动,但是认真考虑:不要这样做。你能想到的每一个选择都比疾病更糟。特别是JUnit 4的@ExpectedException荒谬!它是一个太聪明的框架式解决方案,需要每个人学习它的工作原理,而不是一个普通的自我显然的Java代码。更糟糕的是,它没有办法只包装你期望抛出异常的测试部分,因此如果早期设置步骤抛出相同的异常,即使您的代码已经损坏,您的测试也会通过。
我可以在这里写一篇长篇大论(很抱歉我没有足够的时间),因为我们在Google的Java工程师中对这个问题进行了长时间的讨论,共识是这些疯狂的解决方案都不值得。习惯使用try/catch,它真的没有那么糟糕。

4
也许我错了,但我认为JUnit的设计非常具体,而在其中,@ExpectedException并不是一个太聪明的解决方案。如果你的测试如此复杂以至于你不知道异常可能来自哪里,那么你没有正确使用JUnit。他们会建议你考虑重构你的代码,更好地定义每个类/方法正在做什么。在良好的模块化设计下,我不认为这应该是一个问题。我是否忽略或低估了某些问题? - Andrew Hubbs


0

嗯,如果不是预期的异常,你可以直接捕获异常并重新抛出。尽管良好的编码实践通常要求代码的成功路径不应该由异常来定义,所以你可能需要重新思考你的设计。


我认为出于测试目的,这可能具有价值。 - Mark Peters
MarkPeters的回答大致符合我的意图。尽管它仍然有整个“期望的成功执行路径需要异常,而错误情况则不需要”的问题。它还稍微改变了执行方式,因此意外异常不会重新抛出,而是触发调用失败。在您的原始代码中,您有三条路径,期望的异常,意外的异常上升堆栈,以及没有异常,调用失败。Mark的代码简化为期望的异常和对于意外异常和无异常情况调用失败。 - ShadowRanger
抱歉,在你添加内容之前我已经开始我的评论了。在测试中有潜在的用途,但是我想提醒一下,以防这是为生产代码而设计的。现代异常处理针对无异常情况进行了优化,因此在成功情况下,你将会产生开销,而失败情况将会避免该开销。可以说是非常不合理的。 - ShadowRanger

0

您还可以使用支持实时模板(例如IntellJ IDEA {{link1:)的IDE,并分配快捷键,如 ee -> [tab],以插入try/catch/ignore,让您输入正确的内容。

like this

like this


我确实使用IDEA,但希望将常见代码提取到某个能够很好地表达意图的东西中。 - Alex Baranosky

0

泛型不是类型。它们也不是模板。在Java中,它们是编译时类型检查。异常块按类型捕获。您可以catch(Exception e)甚至catch(Throwable e),然后根据需要进行转换。


0
@FunctionalInterface
 public interface ThrowingConsumer<T, E extends Exception> {
      void accept(T t) throws E;
 }
public static <T, E extends Exception> Consumer<T> errConsumerWrapper(ThrowingConsumer<T, E> throwingConsumer,
                   Class<E> exceptionClass, 
                   Consumer<E> exceptionConsumer) {
            return i -> {
                try {
                    throwingConsumer.accept(i);
                } catch (Exception ex) {
                    try {
                        exceptionConsumer.accept(exceptionClass.cast(ex));
                    } catch (ClassCastException ccEx) {
                        throw new RuntimeException(ex);
                    }
                }
            };
        }

使用示例 Stream.of("a").forEach(errConsumerWrapper(i -> Integer.parseInt(i), NumberFormatException.class, Throwable::printStackTrace));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接