What does the Java assert keyword do, and when should it be used?
有哪些真实的例子可以理解断言的关键作用?
在Java 1.4中添加断言(通过断言关键字)。它们用于验证代码中不变量的正确性。它们不应该在生产代码中被触发,并且表示存在错误或代码路径被滥用。它们可以通过
一个例子:
1 2 3 4 5 6 7 8 9 10 11 | 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; } |
号
假设你要写一个控制核电站的程序。很明显,即使是最微小的错误也可能产生灾难性的结果,因此您的代码必须是无缺陷的(假设为了这个论点,JVM是无缺陷的)。
Java不是一个可验证的语言,这意味着:你不能计算出你的操作结果是完美的。其主要原因是指针:它们可以指向任何地方,也可以不指向任何地方,因此无法将它们计算为该精确值,至少不能在合理的代码范围内。考虑到这个问题,无法证明您的代码在整体上是正确的。但是你能做的就是证明你至少在每一个错误发生的时候都能找到它。
这个想法基于契约式设计(DBC)范式:首先定义(具有数学精度)您的方法应该做什么,然后在实际执行期间通过测试来验证这一点。例子:
1 2 3 4 | // Calculates the sum of a (int) + b (int) and returns the result (int). int sum(int a, int b) { return a + b; } |
虽然这很明显可以很好地工作,但大多数程序员都不会看到其中隐藏的bug(提示:ArianeV因为类似的bug而崩溃)。现在,dbc定义必须始终检查函数的输入和输出,以验证它是否正常工作。Java可以通过断言来做到这一点:
1 2 3 4 5 6 7 | // 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的时间越早,成本越低)。另外:如果代码中有任何bug,您将检测到它。没有任何方法可以让bug溜过去,并在以后引起问题。
这仍然不是无缺陷代码的保证,但它比通常的程序更接近于无缺陷代码。
断言是一个开发阶段工具,用于捕获代码中的错误。它们被设计为易于删除,因此它们不会存在于生产代码中。因此,断言不是您交付给客户的"解决方案"的一部分。它们是内部检查,以确保您所做的假设是正确的。最常见的例子是测试空值。很多方法都是这样写的:
1 2 3 4 5 6 | void doSomething(Widget widget) { if (widget != null) { widget.someMethod(); // ... ... // do more stuff with this widget } } |
在这种方法中,小部件通常不应该是空的。所以如果它为空,代码中有一个bug需要跟踪。但是上面的代码永远不会告诉你这个。因此,为了编写"安全"代码,您还隐藏了一个bug。编写这样的代码要好得多:
1 2 3 4 5 6 7 8 | /** * @param Widget widget Should never be null */ void doSomething(Widget widget) { assert widget != null; widget.someMethod(); // ... ... // do more stuff with this widget } |
号
这样,您就可以尽早捕获这个bug。(在合同中指定此参数不应为空也很有用。)在开发期间测试代码时,请确保打开断言。(说服你的同事也很难做到,我觉得这很烦人。)
现在,您的一些同事将反对此代码,认为您仍然应该进行空检查,以防止在生产中出现异常。在这种情况下,断言仍然有用。你可以这样写:
1 2 3 4 5 6 7 | void doSomething(Widget widget) { assert widget != null; if (widget != null) { widget.someMethod(); // ... ... // do more stuff with this widget } } |
这样,您的同事会为生产代码的空检查感到高兴,但是在开发过程中,当小部件为空时,您就不再隐藏这个bug了。
下面是一个现实世界的例子:我曾经写过一个方法,它比较两个任意值的相等性,其中任何一个值都可以为空:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * 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; } |
。
此代码在该值不为空的情况下委托
一位同事反对我的代码,告诉我我们的许多类都有错误的
1 2 3 4 5 6 7 8 9 |
只有当
我并没有和我的同事就让错误代码留在我们的代码库中的明智性进行毫无结果的辩论,而是在代码中简单地提出了两个断言。在开发阶段,如果我们的一个类不能正确地实现
1 2 3 4 5 6 7 8 9 10 11 | 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; } |
。
要记住的要点是:
断言只是开发阶段的工具。
断言的要点是让您知道是否存在错误,不仅在您的代码中,而且在您的代码库中。(这里的断言实际上会标记其他类中的错误。)
即使我的同事确信我们的类是正确编写的,这里的断言仍然有用。将添加可能无法测试空值的新类,并且此方法可以为我们标记这些错误。
在开发中,您应该始终打开断言,即使您编写的代码不使用断言。默认情况下,对于任何新的可执行文件,我的IDE都设置为始终执行此操作。
断言不会改变代码在生产中的行为,因此我的同事很高兴有空检查,并且即使
另外,您应该通过放入一个将失败的临时断言来测试断言策略,这样您就可以通过日志文件或输出流中的堆栈跟踪来确保收到通知。
很多很好的答案解释了
答案是:几乎从来没有。
断言,作为一个概念,是美妙的。好的代码有很多
然而,在生产中继续进行不变检查是非常重要的。这是因为完美的测试覆盖是不可能的,并且所有的生产代码都会有错误,断言应该有助于诊断和减轻。
因此,最好使用
有时,人们可能会尝试编写一个不变量检查,它确实需要很长的时间来处理(并且经常被调用到足以让它起作用)。但是,这种检查会减慢测试速度,这也是不可取的。这种耗时的检查通常被写成单元测试。然而,出于这个原因,有时使用
不要仅仅因为
(咆哮:JVM开发人员是一群可怕的、过早优化的编码人员。这就是为什么你在Java插件和JVM中听到这么多安全问题的原因。他们拒绝在生产代码中包含基本检查和断言,我们将继续为此付出代价。)
这是最常见的用例。假设您正在打开一个枚举值:
1 2 3 4 5 6 7 8 9 10 11 | switch (fruit) { case apple: // do something break; case pear: // do something break; case banana: // do something break; } |
只要你处理好每一个案子,你就没事了。但总有一天,有人会将fig添加到枚举中,而忘记将其添加到switch语句中。这会产生一个很难捕捉的bug,因为在离开switch语句之前不会感觉到效果。但是,如果你这样写开关,你可以马上抓住它:
1 2 3 4 5 6 7 8 9 10 11 12 13 | switch (fruit) { case apple: // do something break; case pear: // do something break; case banana: // do something break; default: assert false :"Missing enum value:" + fruit; } |
号
断言用于检查post条件和"不应失败"的pre条件。正确的代码决不应该使断言失败;当它们触发时,它们应该指示一个错误(希望在接近问题实际位置的地方)。
断言的一个例子可能是检查是否按正确的顺序调用了一组特定的方法(例如,在
What does the assert keyword in Java do?
号
让我们看看编译后的字节码。
我们将得出结论:
1 2 3 4 5 |
号
生成几乎完全相同的字节码:
1 2 3 4 5 6 7 8 9 10 11 |
当命令行上传递
我们使用
生成合成字段,以便Java只需要在加载时调用EDCOX1 0次,然后缓存结果。另见:"静态合成"是什么意思?
我们可以通过以下方式验证:
1 2 | javac Assert.java javap -c -constants -private -verbose Assert.class |
。
使用Oracle JDK 1.8.0_45,生成了一个合成静态字段(另请参见:"静态合成"的含义是什么?):
1 2 3 | static final boolean $assertionsDisabled; descriptor: Z flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC |
与静态初始值设定项一起:
1 2 3 4 5 6 7 8 | 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 |
。
主要方法是:
1 2 3 4 5 6 7 8 9 10 11 | 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 |
。
我们得出结论:
- 对于EDOCX1,6字节,没有字节码级的支持:它是Java语言的概念。
- 使用系统属性
-Pcom.me.assert=true 可以很好地模拟assert ,以取代命令行上的-ea 和throw new AssertionError() 。
断言允许检测代码中的缺陷。您可以打开断言进行测试和调试,同时在程序处于生产状态时将其关闭。
当你知道某件事是真的时,为什么要断言它?只有当一切正常工作时,这才是真的。如果程序有缺陷,它可能不是真的。在这个过程的早期检测可以让您知道有什么问题。
断言语句的语法有两种形式:
1 2 | assert boolean_expression; assert boolean_expression: error_message; |
下面是一些基本规则,这些规则控制断言应该在哪里使用以及不应该在哪里使用。断言应用于:
正在验证私有方法的输入参数。不适用于公共方法。当传递坏参数时,
程序中的任何地方都可以确保事实的有效性,这几乎可以肯定是真的。
例如,如果您确定它只能是1或2,则可以使用如下断言:
1 2 3 4 5 6 7 8 9 10 | ... if (i == 1) { ... } else if (i == 2) { ... } else { assert false :"cannot happen. i is" + i; } ... |
号
断言不应用于:
正在验证公共方法的输入参数。由于断言可能并不总是被执行,所以应该使用常规的异常机制。
验证对用户输入的内容的约束。同上。
不应用于副作用。
例如,这不是一个正确的用法,因为这里的断言用于其调用
1 2 3 4 5 6 | public boolean doSomething() { ... } public void someMethod() { assert doSomething(); } |
唯一可以证明这一点的情况是,当您试图查明代码中是否启用断言时:
1 2 3 4 5 6 7 |
。
一个真实世界的例子,来自堆栈类(来自Java文章中的断言)
1 2 3 4 5 | public int pop() { // precondition assert !isEmpty() :"Stack is empty"; return stack[--num]; } |
除了这里提供的所有伟大的答案,官方Java SE 7编程指南有一个非常简洁的手册,使用EDOCX1,6,和几个点的例子,当它是一个好的(和,重要的,坏的)使用断言的想法,以及它如何不同于抛出异常。
链接
断言在开发时非常有用。如果您的代码工作正常,则在某些事情无法发生时使用它。它很容易使用,而且可以永远留在代码中,因为它在现实生活中会被关闭。
如果现实生活中有可能出现这种情况,那么你必须处理好它。
我喜欢它,但不知道如何在eclipse/android/adt中打开它。即使在调试时,它似乎也是关闭的。(这里有一个线程,但它指的是"Java VM",它不出现在ADT运行配置中)。
这是我在服务器上为Hibernate/SQL项目写的一个断言。实体bean有两个有效的布尔属性,分别称为isactive和isdefault。每一个都可以有一个值"y"或"n"或空,这被视为"n"。我们希望确保浏览器客户端仅限于这三个值。因此,在这两个属性的设置器中,我添加了这个断言:
1 |
请注意以下内容。
此断言仅用于开发阶段。如果客户发送了一个错误的值,我们将尽早捕获并修复它,早在我们到达生产阶段之前。断言是针对您可以及早发现的缺陷。
这种断言缓慢而低效。没关系。断言是可以随意慢下来的。我们不在乎,因为它们只是开发工具。这不会减慢生产代码的速度,因为断言将被禁用。(在这一点上有一些分歧,我稍后再谈。)这引出了我的下一点。
这个断言没有副作用。我本可以用一个不可修改的静态最终集来测试我的值,但那个集将一直在生产中,在生产中永远不会被使用。
此断言的存在是为了验证客户端的正确操作。因此,当我们到达生产阶段时,我们将确保客户机运行正常,这样我们就可以安全地关闭断言。
有人这样问:如果在生产中不需要断言,为什么不在完成后就把它们拿出来呢?因为当你开始下一个版本的工作时,你仍然需要它们。
有些人认为你不应该使用断言,因为你永远不能确定所有的错误都消失了,所以即使在生产中,你也需要保持它们在周围。所以使用断言语句没有意义,因为断言的唯一好处是您可以关闭它们。因此,根据这种想法,您应该(几乎)永远不要使用断言。我不同意。当然,如果测试属于生产环境,则不应使用断言。但这种测试不属于生产中。这一个是为了捕捉一个不太可能进入生产的bug,所以当您完成后它可能会安全地关闭。
顺便说一句,我可以这样写:
1 | assert value == null || value.equals("Y") || value.equals("N") : value; |
号
这只适用于三个值,但是如果可能值的数目变大,哈希集版本就变得更方便了。我选择了hashset版本来表达我对效率的看法。
默认情况下禁用断言。要启用它们,我们必须使用
使用断言有两种格式:
断言基本上用于调试应用程序,或者用于替换某些应用程序的异常处理,以检查应用程序的有效性。
断言在运行时有效。一个简单的例子,可以很简单地解释整个概念,在这里,Asjt关键字在Java中做什么?(维基答案)。
重述(许多语言,而不仅仅是Java):
"断言"主要在调试过程中被软件开发人员用作调试辅助工具。断言消息不应出现。许多语言提供了一个编译时选项,将导致忽略所有"断言",用于生成"生产"代码。
"异常"是处理各种错误条件的一种简便方法,不管它们是否代表逻辑错误,因为如果您遇到错误条件而无法继续,您可以从任何位置"将它们抛到空中",期望其他人准备"抓住"它们。控制权只需一步,直接从抛出异常的代码转移到捕手的手套上。(接球手可以看到已经发生的呼叫的完整回溯。)
此外,该子例程的调用方不必检查该子例程是否成功:"如果我们现在在这里,它一定成功了,因为否则它将引发异常,我们现在不会在这里!"这种简单的策略使代码设计和调试变得非常简单。
异常方便地允许致命的错误条件是:"规则的异常"。并且,对于它们来说,由代码路径处理,这也是"规则的异常…"。飞球!"
断言是可能被关闭的检查。它们很少被使用。为什么?
- 它们不能用于检查公共方法参数,因为您无法控制它们。
- 它们不应该用于像
result != null 这样的简单检查,因为这样的检查速度非常快,几乎没有什么可节省的。
那么,还剩下什么?昂贵的条件检查真的期望是真实的。一个很好的例子是像rb树这样的数据结构的不变量。实际上,在JDK8的
- 你真的不想在生产中打开它们,因为它们很容易控制运行时间。
- 您可能希望在测试期间打开或关闭它们。
- 在处理代码时,您一定要打开它们。
基本上,"断言真"将通过,"断言假"将失败。让我们看看这将如何工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
默认情况下,不会执行所有