有哪些真实的例子来理解断言的关键作用?


当前回答

我不知道Java中这个特性背后的历史。但是我有一个关于断言可以用来做什么的想法,我认为在这个帖子中没有提到过。

假设你有一个枚举,你打开它:(†)

public enum Veggie {
    CAULIFLOWER,
    CARROT,
}

// Another class
switch (veggie) {
    case CAULIFLOWER: value = 5;
    case CARROT: value = 3;
}

†:也许优秀的Java编译器/工具会静态地捕获丢失的枚举值。在这种情况下,想象一下你发现了一些bug并修复了它。然后,您可以使用下面描述的技术在代码中适当地添加回归测试。


这里没有好的默认情况。所以你要确保你覆盖了所有的值。当你添加一个新的enum变量时,你要确保你更新了switch块:

public enum Veggie {
    CAULIFLOWER,
    CARROT,
    TOMATO,
}

但事实证明,当用户在web应用程序中加载特定视图时,这段代码会被调用。尽管存在这个错误,视图仍然可以持续存在:这将是一个bug,但不值得干扰视图的加载(让我们说用户只是看到了一些错误的数字)。所以你只需要记录一个警告:

switch (veggie) {
    case CAULIFLOWER: value = 5;
    case CARROT: value = 3;
    default: nonExhaustiveMatchOnEnum();
// […]
public static void nonExhaustiveMatchOnEnum() {
    String errorMessage: "Missing veggie";
    logger.warn(errorMessage);
}

但是当你在开发代码时,你确实希望这个错误立即失败——开发者可以在五分钟内修复这个错误,而不像你的web应用程序的用户。

所以你可以添加一个assert false:

public static void nonExhaustiveMatchOnEnum() {
    String errorMessage: "Missing veggie";
    assert false : errorMessage;
    logger.warn(errorMessage);
}

现在,应用程序将在本地崩溃,但只在生产中记录一个警告(假设您在本地使用java -ea,而不是在生产中)。

其他回答

一个真实的例子,来自一个stack类(来自Java文章中的断言)

public int pop() {
   // precondition
   assert !isEmpty() : "Stack is empty";
   return stack[--num];
}

让我们假设您要编写一个控制核电站的程序。很明显,即使是最微小的错误也可能导致灾难性的结果,因此您的代码必须是无bug的(为了论证,假设JVM是无bug的)。

Java不是一种可验证的语言,这意味着:你不能计算出你的操作结果会是完美的。这样做的主要原因是指针:它们可以指向任何地方,也可以指向任何地方,因此它们不能被计算为这个确切的值,至少在合理的代码范围内不能。对于这个问题,没有办法证明您的代码在整体上是正确的。但你能做的是证明你至少能在bug发生时找到它。

此思想基于契约式设计(Design-by-Contract, DbC)范式:首先定义(具有数学精度)您的方法应该做什么,然后在实际执行期间通过测试来验证这一点。例子:

// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
  return a + b;
}

虽然这很明显可以正常工作,但大多数程序员不会看到其中隐藏的bug(提示:Ariane V因为类似的bug而崩溃)。现在DbC定义您必须始终检查函数的输入和输出,以验证它是否正确工作。Java可以通过断言来做到这一点:

// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
    assert (Integer.MAX_VALUE - a >= b) : "Value of " + a + " + " + b + " is too large to add.";
  final int result = a + b;
    assert (result - a == b) : "Sum of " + a + " + " + b + " returned wrong sum " + result;
  return result;
}

如果这个函数现在失败了,您会注意到它。你会知道你的代码中有问题,你知道它在哪里,你知道是什么引起的(类似于异常)。更重要的是:当它发生时停止正确执行,以防止任何进一步的代码使用错误的值,并可能对它所控制的任何东西造成损害。

Java异常是一个类似的概念,但它们不能验证所有内容。如果需要更多的检查(以降低执行速度为代价),则需要使用断言。这样做会使代码膨胀,但最终可以在短得惊人的开发时间内交付产品(越早修复bug,成本就越低)。此外,如果代码中有任何错误,您将检测到它。不可能出现漏洞并在以后引起问题。

这仍然不能保证代码没有错误,但它比通常的程序更接近于这一点。

默认情况下,断言是禁用的。要启用它们,我们必须使用-ea选项运行程序(粒度可以改变)。例如,java -ea AssertionsDemo。

使用断言有两种格式:

简单:如。维护1 = = 2;//这将引发AssertionError。 更好的方法:断言1==2:“不可能..1不等于2”; 这将引发AssertionError,并显示给定的消息,因此更好。虽然实际的语法是断言expr1:expr2,其中expr2可以是返回值的任何表达式,但我更多地使用它只是为了打印消息。

断言是可以被关闭的检查。它们很少被使用。为什么?

它们不能用于检查公共方法参数,因为您无法控制它们。 它们不应该用于简单的检查,如result != null,因为这样的检查非常快,几乎没有什么可以保存。

那么,还剩下什么呢?昂贵的条件检查是真实的。像rb树这样的数据结构的不变量就是一个很好的例子。实际上,在JDK8的ConcurrentHashMap中,对于TreeNodes有一些这样有意义的断言。

您确实不希望在生产环境中启用它们,因为它们很容易控制运行时间。 您可能希望在测试期间打开或关闭它们。 在处理代码时一定要打开它们。


有时候,支票不是很贵,但与此同时,你很确定,它会通过。在我的代码中,有例如,

assert Sets.newHashSet(userIds).size() == userIds.size();

我很确定我刚刚创建的列表有独特的元素,但我想记录并再次检查它。

下面是我在Hibernate/SQL项目的服务器中编写的断言。一个实体bean有两个有效的布尔属性,称为isActive和isDefault。每个都可以有一个值“Y”或“N”或null,被视为“N”。我们希望确保浏览器客户端仅限于这三个值。所以,在这两个属性的setter中,我添加了这样的断言:

assert new HashSet<String>(Arrays.asList("Y", "N", null)).contains(value) : value;

注意以下几点。

This assertion is for the development phase only. If the client sends a bad value, we will catch that early and fix it, long before we reach production. Assertions are for defects that you can catch early. This assertion is slow and inefficient. That's okay. Assertions are free to be slow. We don't care because they're development-only tools. This won't slow down the production code because assertions will be disabled. (There's some disagreement on this point, which I'll get to later.) This leads to my next point. This assertion has no side effects. I could have tested my value against an unmodifiable static final Set, but that set would have stayed around in production, where it would never get used. This assertion exists to verify the proper operation of the client. So by the time we reach production, we will be sure that the client is operating properly, so we can safely turn the assertion off. Some people ask this: If the assertion isn't needed in production, why not just take them out when you're done? Because you'll still need them when you start working on the next version.

Some people have argued that you should never use assertions, because you can never be sure that all the bugs are gone, so you need to keep them around even in production. And so there's no point in using the assert statement, since the only advantage to asserts is that you can turn them off. Hence, according to this thinking, you should (almost) never use asserts. I disagree. It's certainly true that if a test belongs in production, you should not use an assert. But this test does not belong in production. This one is for catching a bug that's not likely to ever reach production, so it may safely be turned off when you're done.

顺便说一句,我可以这样写:

assert value == null || value.equals("Y") || value.equals("N") : value;

对于只有三个值是可以的,但是如果可能值的数量变大,HashSet版本就会变得更方便。我选择HashSet版本来说明我关于效率的观点。