在Java中主动抛出AssertionError是一种好的实践吗?

30

在阅读Joshua Bloch的《Effective Java - 第二版》时,我在第152页上发现了以下代码:

double apply(double x, double y) {
    switch(this) {
        case PLUS:   return x + y;
        case MINUS:  return x - y;
        case TIMES:  return x * y;
        case DIVIDE: return x / y;
    }
    throw new AssertionError("Unknown op: " + this);
}

现在让我困惑的是,为什么会主动抛出 AssertionError?这被认为是好习惯吗?
根据我的理解,断言是用来不干扰代码的,这样当启动 Java 编程时没有启用断言且因此未执行 assert 语句时,行为不会改变。
如果我在运行程序时甚至还没有启用断言,却收到 AssertionException,那我就很困惑了。
尽管我明白这种示例情况可能经常发生,即分析几个不同的选项,如果都不是,则应抛出异常。
所以,在这里抛出 AssertionException 是好习惯吗?还是更好地抛出其他异常?如果是,哪个最适合呢?也许是IllegalArgumentException
编辑以澄清问题:我的问题不在于我们是否应该在这里抛出 Error,而是如果我们想要抛出 ExceptionError,应该选择哪个?在活跃地抛出 AssertionError 是否是好习惯?文档中说“抛出以指示断言失败”,所以我觉得我们不应该主动抛出它。这正确吗?
第二次编辑:清晰明确的问题:活跃地抛出 AssertionError 是好习惯吗,还是应该避免,即使可能性存在?(通过阅读文档,我猜测是后者)

1
这是非常主观的。就我而言,我会抛出AssertionError。我认为在生产环境中不检查断言的主要原因是检查断言的成本,在问题的示例中我认为不存在这样的成本。我站在Bloch的立场上。 - Ole V.V.
1
这绝对是一个错误,而不是异常。没有合理的情况下应该尝试捕获错误并继续执行。 - Hot Licks
从我在这里的讨论中所了解的来回答标题问题:是的,在您的程序中积极抛出AssertionError是可以的。如果这样做,断言无需打开。 - Mathias Bader
@MathiasBader - 这是你的程序,你可以做任何你想做的事情。 - Hot Licks
6个回答

26

在这里,我同意Bloch先生的观点——替代方案(IllegalArgumentException, IllegalStateExceptionUnsupportedOperationException)没有恰当地传达问题的严重性,调用者可能会错误地尝试捕获和处理此情况。实际上,如果该行代码被执行,那么所涉及的程序是错误的,唯一合理的事情是退出。

关键在于枚举具有有限的一组值,因此不可能到达throw语句——只有在修改了枚举定义但未修复此实例方法时才会出现。抛出RuntimeException表明调用者犯了错误,而事实上该方法(以及枚举本身)是损坏的。明确引发AssertionError正确指示该方法期望的不变量已被违反。

Guava有一篇很有帮助的文章,介绍了何时引发不同类型的异常。他们写道:

传统的断言是一个检查,只有在包含检查的类本身(在某些情况下可以扩展到包)出现故障时才会失败。这些可以采取各种形式,包括后置条件、类不变量和内部前置条件(在非公共方法上)。

不可能条件检查是一种不可能失败的检查,除非后续代码被修改,或者我们对平台行为的最深层次的假设被严重违反。这些应该是不必要的,但通常是被强制的,因为编译器无法识别语句是不可达的,或者因为我们知道关于控制流的某些信息,而编译器无法推断。

该页面建议使用 AssertionError 处理这些情况。在他们的Verify类中的注释还提供了一些关于选择异常的有用见解。在 AssertionError 看来过于强硬的情况下,抛出 VerifyException 可以是一个很好的折中方案。

至于具体问题的 ErrorRuntimeException,其实并不重要(两者都是未经检查的,因此可能会在调用堆栈上行进而不被捕获),但调用者更容易尝试从 RuntimeException 中恢复。在这种情况下崩溃应用程序是一种特性,因为否则我们将继续运行一个(此时)明显有误的应用程序。当然,调用者可以随意处理和捕获 AssertionError(或 ErrorThrowable), 但这样的情况比较少发生。


那么你认为在某些情况下主动抛出AssertionError是可以的吗?或者这只应该在使用assert语句时由编译器完成? - Mathias Bader
当然;AssertionError 表示程序的不变量被违反了。 - dimo414

6

在我看来,这里使用AssertionError是不正确的。

从文档中得知AssertionError扩展了基类Error

Error是Throwable的一个子类,表示应用程序不应尝试捕获的严重问题。

错误应该是致命的,而我希望您的程序处理此问题,并向用户显示有关未知操作的警告消息。

如果有什么问题,我希望抛出UnsupportedOperationException,并在调用堆栈的其他位置进行处理。

抛出以指示不支持所请求的操作。

考虑一种情况,不是在计算器中,而是在使用ENUM的任何代码流程中:

如果开发人员向现有枚举添加新值,我不希望使用该现有枚举的函数因为不支持新值而出错。

当然,我们是否应该显示一条消息还是抛出一个错误可以有争议。但让我们假设我们想抛出一个错误:哪个是正确的?它真的可以是AssertionError吗?文档也说“抛出以指示断言失败”。但是这里根本没有断言-所以我感觉这违反了文档。对吗? - Mathias Bader
在这种情况下使用AssertionError并不错,因为它断言此情况永远不会被触发。 - Gurwinder Singh
5
我不同意;因为枚举类型有一个有限的参数集,所以达到throw行应该是不可能的 - 它只会在枚举定义发生更改时发生。抛出IAEUOE表明调用者犯了一个错误,而实际上方法是有问题的。一个AssertionError正确地表明了这个方法期望的不变量仍然存在。 - dimo414
3
我支持dimo的想法。如果我们遇到了那个“AssertionError”,这并不意味着调用者做了他们不应该做的事情;它意味着我们处于一个本应该不可能出现的情况下。通常情况下,这是我们认为不会发生的情况,但是“assert”语句并不足以满足编译器的可达性分析,因此我们直接抛出一个“AssertionError”。 - user2357112
3
请注意,该示例是枚举本身的一个方法。如果枚举作为参数,根据枚举和方法之间的关系,“IAE”可能是合理的,但由于它是枚举本身的一部分,如果在添加新的枚举值时不修复该方法,那么该方法就会被定义错误。 - dimo414
显示剩余4条评论

5
关于错误,Java教程指出:

第二种异常是错误。这些异常情况是应用程序外部的,通常应用程序无法预测或从中恢复。

此外,使用断言进行编程指南也指出:

不要在公共方法中使用断言进行参数检查。

因此,我认为异常是检查这种情况的正确方式。
我建议使用new UnsupportedOperationException("Operator " + name() + " is not supported.");,因为在我看来它更好地描述了问题(即开发人员添加了枚举值但忘记实现所需的情况)。
但是,我认为这个示例案例应该使用AbstractEnum设计模式而不是switch语句:
PLUS {
    double apply(double x, double y) {
        return x + y;
    }
},
MINUS {
    double apply(double x, double y) {
        return x - y;
    }
},
TIMES {
    double apply(double x, double y) {
        return x * y;
    }
},
DIVIDE {
    double apply(double x, double y) {
        return x / y;
    }
};

abstract double apply(double x, double y);

由于这份代码不会编译直到每个case实现了apply,因此它的错误率较低。


1
“不要在公共方法中使用断言进行参数检查。” Bloch 的例子并没有检查公共方法的参数 - 它断言类本身被正确定义。我建议阅读《Effective Java》中引用的章节;他继续探讨了您描述的模式,并解释了为什么有时使用枚举更可取。 - dimo414

3

I would prefer

    double apply(double x, double y) {
    switch(this) {
        case PLUS:   return x + y;
        case MINUS:  return x - y;
        case TIMES:  return x * y;
        default: assert this==DIVIDE: return x / y;
    }
}
  1. 我们不应该抛出AssertionError,因为它应该保留给实际断言使用。
  2. 除了断言和某些捕获块之外,不应该有任何不能实际达到的代码。

但我更喜欢https://dev59.com/Z1gR5IYBdhLWcg3wbs6X#41324246


1
我认为无论是 AssertionError 还是 IllegalAE 在这里都不太合适。正如 Matt 的回答所指出的那样,Assertion Error 不好。而这些参数并没有错,只是在错误的 this 操作上传递给了一个方法。因此 IAE 也可能不太合适。当然,这也是一个基于观点的问题和答案。
另外,我不确定启用断言是否强制要求抛出 AssertionError 或者 AssertionError 是否意味着已启用断言。

0
据我理解,您的方法是枚举对象的一种方法。在大多数情况下,当有人添加新的枚举值时,他还应修改“apply”方法。这种情况下,您应该抛出UnsupportedOperationException异常。

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