Java断言关键字做什么,什么时候应该使用它?

什么是现实生活中的例子来理解断言的关键作用?

561707 次浏览

一个真实世界的例子,来自Stack类(来自Java文章中的断言

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

断言用于检查后置条件和“永远不应该失败”的前置条件。正确的代码永远不应该使断言失败;当它们触发时,它们应该指示bug(希望在接近问题实际轨迹的地方)。

断言的一个示例可能是检查特定的方法组是否以正确的顺序调用(例如,在Iterator中,hasNext()next()之前调用)。

断言(通过断言关键字)在Java1.4中添加。它们用于验证代码中不变量的正确性。它们永远不应该在生产代码中触发,并且指示代码路径的bug或误用。它们可以在运行时通过java命令上的-ea选项激活,但默认情况下不打开。

举个例子:

public Foo acquireFoo(int id) {
Foo result = null;
if (id > 50) {
result = fooService.read(id);
} else {
result = new Foo(id);
}
assert result != null;


return result;
}

断言基本上用于调试应用程序,或者用于替换某些应用程序的异常处理以检查应用程序的有效性。

断言在运行时工作。这里有一个简单的例子,可以非常简单地解释整个概念-断言关键字在Java做什么?(WikiAnswers)。

assert是一个关键字。它是在JDK 1.4中引入的。有两种类型的assert

  1. 非常简单的assert语句
  2. 简单的assert语句。

默认情况下,不会执行所有assert语句。如果assert语句收到false,则会自动引发断言错误。

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

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

这个想法基于按合同设计(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;
}

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

Java异常是一个类似的概念,但它们无法验证所有内容。如果你想要更多的检查(以执行速度为代价),你需要使用断言。这样做会使你的代码膨胀,但你最终可以在非常短的开发时间内交付产品(越早修复bug,成本越低)。此外:如果你的代码中有任何bug,你会检测到它。bug不可能滑过并在以后引起问题。

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

这是最常见的用例。假设您要打开枚举值:

switch (fruit) {
case apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
}

只要你处理好每一个case,你就没问题。但是有一天,有人会将fig添加到你的枚举中,而忘记将其添加到你的Switch语句中。这产生了一个可能很难捕捉到的bug,因为直到你离开Switch语句后才会感受到效果。但是如果你像这样写你的Switch,你可以立即捕捉到它:

switch (fruit) {
case apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
default:
assert false : "Missing enum value: " + fruit;
}

断言是一个开发阶段的工具,用于捕获代码中的错误。它们旨在轻松删除,因此它们不会存在于生产代码中。因此断言不是你交付给客户的“解决方案”的一部分。它们是内部检查,以确保你做出的假设是正确的。最常见的例子是测试null。许多方法是这样编写的:

void doSomething(Widget widget) {
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}

通常在这样的方法中,小部件永远不应该为空。所以如果它为空,说明你的代码中某个地方有bug,你需要跟踪。但上面的代码永远不会告诉你这一点。所以为了编写“安全”的代码,你还隐藏了一个bug。这样写代码要好得多:

/**
* @param Widget widget Should never be null
*/
void doSomething(Widget widget) {
assert widget != null;
widget.someMethod(); // ...
... // do more stuff with this widget
}

这样,你就可以确保尽早发现这个bug。(在契约中指定这个参数永远不应该为空也很有用。)在开发过程中测试代码时,一定要打开断言。(说服你的同事这样做通常也很困难,我觉得很烦人。)

现在,你的一些同事会反对这段代码,认为你仍然应该放入空检查以防止生产中的异常。在这种情况下,断言仍然有用。你可以这样写:

void doSomething(Widget widget) {
assert widget != null;
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}

这样,您的同事会很高兴产品代码有空值检查,但在开发过程中,当小部件为空时,您不再隐藏bug。

这是一个真实世界的例子:我曾经写过一个方法来比较两个任意值的相等性,其中任何一个值都可以为null:

/**
* Compare two values using equals(), after checking for null.
* @param thisValue (may be null)
* @param otherValue (may be null)
* @return True if they are both null or if equals() returns true
*/
public static boolean compare(final Object thisValue, final Object otherValue) {
boolean result;
if (thisValue == null) {
result = otherValue == null;
} else {
result = thisValue.equals(otherValue);
}
return result;
}

在thisValue不为null的情况下,此代码委托equals()方法的工作。但它假设equals()方法通过正确处理空参数正确满足equals()的约定。

一位同事反对我的代码,他告诉我我们的许多类都有不能测试null的错误equals()方法,所以我应该在这个方法中进行检查。这是否明智,或者我们是否应该强制执行错误,以便我们发现并修复它,这是有争议的,但我推迟了我的同事并进行了空检查,我用注释标记了它:

public static boolean compare(final Object thisValue, final Object otherValue) {
boolean result;
if (thisValue == null) {
result = otherValue == null;
} else {
result = otherValue != null && thisValue.equals(otherValue); // questionable null check
}
return result;
}

这里的附加检查other != null仅在equals()方法未能按照其合约要求检查null时才需要。

我没有与我的同事就让错误代码留在我们的代码库中是否明智进行徒劳的辩论,而是简单地在代码中添加了两个断言。在开发阶段,如果我们的一个类未能正确实现equals(),这些断言会让我知道,所以我可以修复它:

public static boolean compare(final Object thisValue, final Object otherValue) {
boolean result;
if (thisValue == null) {
result = otherValue == null;
assert otherValue == null || otherValue.equals(null) == false;
} else {
result = otherValue != null && thisValue.equals(otherValue);
assert thisValue.equals(null) == false;
}
return result;
}

要记住的要点是:

  1. 断言只是开发阶段的工具。

  2. 断言的目的是让您知道是否有bug,不仅在您的代码中,而且在您的代码库中。(这里的断言实际上会标记其他类中的错误。)

  3. 即使我的同事确信我们的类是正确编写的,这里的断言仍然有用。将添加可能无法测试null的新类,并且此方法可以为我们标记这些错误。

  4. 在开发中,您应该始终打开断言,即使您编写的代码不使用断言。我的IDE默认设置为对任何新可执行文件始终这样做。

  5. 断言不会改变生产环境中代码的行为,所以我的同事很高兴空检查存在,并且即使equals()方法有错误,这个方法也会正确执行。我很高兴,因为我会在开发中捕获任何有错误的equals()方法。

此外,您应该通过输入一个会失败的临时断言来测试您的断言策略,这样您就可以确定您通过日志文件或输出流中的堆栈跟踪收到了通知。

断言在开发时非常很有用。如果您的代码正常工作,您可以在不能发生某些事情时使用它。它易于使用,并且可以永远留在代码中,因为它在现实生活中会被关闭。

如果这种情况有可能发生在现实生活中,那么你必须处理它。

我喜欢它,但不知道如何在Eclipse/Android/ADT中打开它。即使在调试时,它似乎也是关闭的。(这上面有一个线程,但它指的是“Javavm”,它没有出现在ADT运行配置中)。

基本上,“断言为真”将通过,“断言为假”将失败。让我们看看这是如何工作的:

public static void main(String[] args)
{
String s1 = "Hello";
assert checkInteger(s1);
}


private static boolean checkInteger(String s)
{
try {
Integer.parseInt(s);
return true;
}
catch(Exception e)
{
return false;
}
}

Java中的assert关键字是做什么的?

让我们看看编译的字节码。

我们的结论是:

public class Assert {
public static void main(String[] args) {
assert System.currentTimeMillis() == 0L;
}
}

生成几乎完全相同的字节码:

public class Assert {
static final boolean $assertionsDisabled =
!Assert.class.desiredAssertionStatus();
public static void main(String[] args) {
if (!$assertionsDisabled) {
if (System.currentTimeMillis() != 0L) {
throw new AssertionError();
}
}
}
}

-ea在命令行上传递时,其中Assert.class.desiredAssertionStatus()true,否则为false。

我们使用System.currentTimeMillis()来确保它不会被优化掉(assert true;做到了)。

合成字段的生成使Java只需要在加载时调用Assert.class.desiredAssertionStatus()一次,然后将结果缓存在那里。

我们可以通过以下方式验证:

javac Assert.java
javap -c -constants -private -verbose Assert.class

使用Oracle JDK 1.8.0_45,生成了一个合成静态字段(另请参阅:“静态合成”的含义是什么?):

static final boolean $assertionsDisabled;
descriptor: Z
flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC

加上一个静态构造器:

 0: ldc           #6                  // class Assert
2: invokevirtual #7                  // Method java/lang Class.desiredAssertionStatus:()Z
5: ifne          12
8: iconst_1
9: goto          13
12: iconst_0
13: putstatic     #2                  // Field $assertionsDisabled:Z
16: return

主要方法是:

 0: getstatic     #2                  // Field $assertionsDisabled:Z
3: ifne          22
6: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
9: lconst_0
10: lcmp
11: ifeq          22
14: new           #4                  // class java/lang/AssertionError
17: dup
18: invokespecial #5                  // Method java/lang/AssertionError."<init>":()V
21: athrow
22: return

我们的结论是:

  • 没有对assert的字节码级支持:它是一个Java语言概念
  • assert可以很好地模拟系统属性-Pcom.me.assert=true以替换命令行上的-eathrow new AssertionError()

除了这里提供的所有很好的答案之外,官方JavaSE 7编程指南还有一个关于使用assert;的非常简洁的手册,其中有几个现成的例子,说明什么时候使用断言是一个好主意(重要的是,坏主意),以及它与抛出异常有何不同。

链接

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

使用断言有两种格式:

  1. 简单:例如。assert 1==2; // This will raise an AssertionError
  2. 更好:assert 1==2: "no way.. 1 is not equal to 2"; 这将引发AssertionError并显示给定的消息,因此更好。虽然实际语法是assert expr1:expr2,其中expr2可以是任何返回值的表达式,但我更经常使用它来打印消息。

什么时候应该使用断言?

很多很好的答案解释了assert关键字的作用,但很少回答真正的问题,“什么时候应该在现实生活中使用assert关键字?”答案:

几乎从不

断言作为一个概念是很棒的。好的代码有很多if (...) throw ...语句(以及它们的亲戚,如Objects.requireNonNullMath.addExact)。然而,某些设计决策极大地限制了assert关键词本身的实用性。

assert关键字背后的驱动思想是过早优化,主要功能是能够轻松关闭所有检查。事实上,默认情况下,assert检查是关闭的。

然而,在生产中继续进行不变检查是至关重要的。这是因为完美的测试覆盖是不可能的,所有的生产代码都会有错误,断言应该有助于诊断和减轻。

因此,应该首选使用if (...) throw ...,就像检查公共方法的参数值和抛出IllegalArgumentException一样。

偶尔,人们可能会试图编写一个不变检查,该检查确实需要很长的时间来处理(并且调用的频率足以让它重要)。然而,这种检查会减慢测试速度,这也是不可取的。这种耗时的检查通常被编写为单元测试。然而,出于这个原因,有时使用assert可能是有意义的。

不要仅仅因为它比if (...) throw ...干净漂亮而使用assert(我说得很痛苦,因为我喜欢干净漂亮)。如果你只是无法控制自己,并且可以控制你的应用程序如何启动,那么请随意使用assert,但始终在生产中启用断言。不可否认,这是我倾向于做的。我正在推动一个lombok注释,这将导致assert的行为更像if (...) throw ...在这里投票。

(咆哮:JVM开发人员是一群糟糕的、过早优化的程序员。这就是为什么你在Java插件和JVM中听到这么多安全问题。他们拒绝在生产代码中包含基本的检查和断言,我们正在继续付出代价。)

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

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

注意以下几点。

  1. 此断言仅适用于开发阶段。如果客户端发送了错误的值,我们将及早发现并修复它,早在我们进入生产之前。断言适用于您可以及早发现的缺陷。

  2. 这种断言很慢而且效率低下。没关系。断言可以很慢。我们不在乎,因为它们是仅限开发的工具。这不会减慢生产代码的速度,因为断言将被禁用。(在这一点上存在一些分歧,我稍后会讨论。)这导致了我的下一点。

  3. 这个断言没有副作用。我本可以用一个不可修改的静态最终集合来测试我的值,但是那个集合会留在生产中,在那里它永远不会被使用。

  4. 此断言的存在是为了验证客户端的正确操作。因此,当我们到达生产环境时,我们将确保客户端运行正常,因此我们可以安全地关闭断言。

  5. 有些人会问:如果在生产中不需要断言,为什么不在完成后把它们拿出来呢?因为当你开始下一个版本的工作时,你仍然需要它们。

有些人认为你永远不应该使用断言,因为你永远无法确定所有的错误都消失了,所以即使在生产环境中你也需要保留它们。因此,使用断言语句是没有意义的,因为断言的唯一优势是你可以关闭它们。因此,根据这种想法,你应该(几乎)永远不使用断言。我不同意。如果一个测试属于生产环境,你就不应该使用断言,这当然是真的。但是这个测试没有确实属于生产环境。这个是为了捕捉一个不太可能到达生产环境的bug,所以当你完成后它可以安全地关闭。

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

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

这仅适用于三个值,但如果可能值的数量变得更大,HashSet版本会变得更方便。我选择HashSet版本是为了说明我对效率的看法。

断言允许检测代码中的缺陷。您可以打开断言进行测试和调试,同时在程序处于生产状态时将它们关闭。

当您知道某事是真的时,为什么要断言它?只有当一切正常工作时,它才是真的。如果程序有缺陷,它可能实际上不是真的。在过程的早期检测到这一点可以让您知道有问题。

assert语句包含此语句以及可选的String消息。

断言语句的语法有两种形式:

assert boolean_expression;
assert boolean_expression: error_message;

以下是一些基本规则,它们决定了断言应该在哪里使用和不应该在哪里使用。断言应该用于:

  1. 验证私有方法的输入参数。对于公共方法。public方法应该在传递坏参数时抛出常规异常。

  2. 程序中的任何地方,以确保几乎可以肯定的事实的有效性。

例如,如果您确定它只会是1或2,您可以使用这样的断言:

...
if (i == 1)    {
...
}
else if (i == 2)    {
...
} else {
assert false : "cannot happen. i is " + i;
}
...
  1. 在任何方法的末尾验证post条件。这意味着,在执行业务逻辑之后,您可以使用断言来确保变量或结果的内部状态与您期望的一致。例如,打开套接字或文件的方法可以在末尾使用断言来确保套接字或文件确实被打开。

断言不应该用于:

  1. 验证公共方法的输入参数。由于断言可能并不总是被执行,因此应该使用常规的异常机制。

  2. 验证对用户输入的内容的约束。同上。

  3. 不应该用于副作用。

例如,这不是一个正确的用法,因为这里的断言用于调用doSomething()方法的副作用。

public boolean doSomething() {
...    
}
public void someMethod() {       
assert doSomething();
}

唯一可以证明这一点的情况是,当您试图找出代码中是否启用了断言时:   

boolean enabled = false;    
assert enabled = true;    
if (enabled) {
System.out.println("Assertions are enabled");
} else {
System.out.println("Assertions are disabled");
}

总结(许多语言都是如此,不仅仅是Java):

“断言”主要被软件开发人员在调试过程中用作调试辅助工具。断言消息不应该出现。许多语言提供了一个编译时选项,该选项将导致所有“断言”被忽略,用于生成“生产”代码。

“异常”是处理各种错误条件的便捷方法,无论它们是否代表逻辑错误,因为如果你遇到无法继续的错误条件,你可以简单地“将它们抛向空中”,无论你身在何处,期待其他人准备好“抓住”它们。控制权一步到位,直接从抛出异常的代码直接转移到捕手的手套。(捕手可以看到已经发生的呼叫的完整回溯。

此外,该子例程的调用者不必检查该子例程是否成功:“如果我们现在在这里,它一定成功了,因为否则它会抛出异常,我们现在就不会在这里!”这个简单的策略使代码设计和调试变得更加容易。

异常方便地允许致命错误条件成为它们的样子:“规则的异常”。并且,为了让它们由同时也是“规则的异常…“飞球!”

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

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

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

  • 您真的不想在生产中打开它们,因为它们可以轻松控制运行时间。
  • 您可能希望在测试期间打开或关闭它们。
  • 您肯定希望在处理代码时打开它们。

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

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

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

这是另一个例子。我写了一个方法来查找两个已排序数组中值的中位数。该方法假设数组已经排序。出于性能原因,它不应该首先对数组进行排序,甚至不应该检查以确保它们已排序。然而,使用未排序的数据调用此方法是一个严重的bug,我们希望在开发阶段及早发现这些错误。所以我是如何处理那些看似冲突的目标的:

public static int medianOf(int[] a, int[] b) {
assert assertionOnlyIsSorted(a);      // Assertion is order n
assert assertionOnlyIsSorted(b);
... // rest of implementation goes here. Algorithm is order log(n)
}


public static boolean assertionOnlyIsSorted(int[] array) {
for (int i=1; i<array.length; ++i) {
if (array[i] < array[i-1]) {
return false;
}
return true;
}
}

这样,测试很慢,只在开发阶段执行,在开发阶段,速度不如捕捉错误重要。您希望medianOf()方法具有log(n)性能,但“is sorted”测试的顺序为n。所以我把它放在一个断言中,将其使用限制在开发阶段,并且我给它起了一个名字,表明它不适合生产。

这样我就两全其美了。在开发中,我知道任何错误调用this的方法都会被捕获并修复。而且我知道这样做的缓慢测试不会影响生产中的性能。(这也很好地说明了为什么你想在生产中关闭断言,但在开发中打开它们。)

我不知道Java这个特性背后的历史是什么。但是我有一个关于assert可以用来做什么的想法,我认为这个线程没有提到。

假设你有一个你打开的枚举:()

public enum Veggie {
CAULIFLOWER,
CARROT,
}


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

也许好的Java编译器/工具会静态捕获缺失的枚举值。在这种情况下,想象你找到了一些bug并修复了它。然后你可以使用下面描述的技术在代码中添加一个回归测试。


没有好的默认案例。所以您真的要确保涵盖所有值。您要确保每当添加新的枚举变体时都会更新开关块:

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);
}

但是,您希望在开发代码时立即失败-开发人员可以在五分钟内修复这样的bug,而不是Web应用程序的用户。

所以你可以添加一个assert false

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

现在,应用程序将在本地崩溃,但仅在生产环境中记录警告(假设您在本地使用java -ea,但未在生产环境中使用)。