有哪些现实生活中的例子可以理解断言的关键作用?
19 回答
Java 1.4 中添加了断言(通过assert关键字)。它们用于验证代码中不变量的正确性。它们永远不应在生产代码中触发,并且表明存在错误或代码路径的滥用。它们可以在运行时通过命令-ea
上的选项激活java
,但默认情况下不启用。
一个例子:
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 不是一种可验证的语言,这意味着:您无法计算出您的操作结果会是完美的。这样做的主要原因是指针:它们可以指向任何地方或不指向任何地方,因此它们不能被计算为这个精确值,至少不在合理的代码范围内。鉴于这个问题,没有办法证明你的代码整体上是正确的。但是你能做的是证明你至少能在每一个 bug 发生时找到它。
这个想法基于按合同设计(DbC) 范式:您首先定义(以数学精度)您的方法应该做什么,然后通过在实际执行期间对其进行测试来验证这一点。例子:
// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
return a + b;
}
虽然这很明显可以正常工作,但大多数程序员不会看到其中隐藏的错误(提示:Ariane V 因类似错误而崩溃)。现在 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 异常是一个类似的概念,但它们无法验证所有内容。如果您想要更多检查(以执行速度为代价),您需要使用断言。这样做会使您的代码膨胀,但您最终可以在极短的开发时间内交付产品(修复错误越早,成本越低)。此外:如果您的代码中有任何错误,您将检测到它。没有办法让错误溜走并在以后引起问题。
这仍然不能保证代码没有错误,但它比通常的程序更接近这一点。
断言是一种用于捕获代码中错误的开发阶段工具。它们被设计为易于删除,因此它们不会存在于生产代码中。因此,断言不是您交付给客户的“解决方案”的一部分。它们是内部检查,以确保您所做的假设是正确的。最常见的例子是测试空值。很多方法都是这样写的:
void doSomething(Widget widget) {
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
很多时候,在这样的方法中,小部件永远不应该为空。因此,如果它为空,则您的代码中存在您需要追踪的错误。但是上面的代码永远不会告诉你这一点。因此,在编写“安全”代码的善意努力中,您也隐藏了一个错误。编写这样的代码要好得多:
/**
* @param Widget widget Should never be null
*/
void doSomething(Widget widget) {
assert widget != null;
widget.someMethod(); // ...
... // do more stuff with this widget
}
这样,您一定会尽早发现此错误。(在合同中指定此参数永远不应为空也很有用。)在开发期间测试代码时,请务必打开断言。(说服你的同事这样做也很困难,我觉得这很烦人。)
现在,您的一些同事会反对此代码,认为您仍应进行 null 检查以防止生产中出现异常。在这种情况下,断言仍然有用。你可以这样写:
void doSomething(Widget widget) {
assert widget != null;
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
这样,您的同事会很高兴对生产代码进行空检查,但在开发过程中,当小部件为空时,您不再隐藏错误。
这是一个真实的例子:我曾经写过一个比较两个任意值是否相等的方法,其中任何一个值都可以为空:
/**
* 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;
}
此代码equals()
在 thisValue 不为 null 的情况下委托方法的工作。但它假设该方法通过正确处理空参数equals()
正确履行了合同。equals()
一位同事反对我的代码,告诉我我们的许多类都有equals()
不测试 null 的错误方法,所以我应该把检查放到这个方法中。这是否明智,或者我们是否应该强制错误,所以我们可以发现它并修复它,这是值得商榷的,但我听从了我的同事并进行了空检查,我已经用评论标记了它:
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;
}
仅当该方法未能按照其合同的要求检查 null 时,此处的附加检查other != null
才是必需的。equals()
我没有与我的同事就让有缺陷的代码留在我们的代码库中是否明智而进行毫无结果的辩论,我只是在代码中添加了两个断言。这些断言会让我知道,在开发阶段,如果我们的某个类无法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;
}
要记住的要点是:
断言只是开发阶段的工具。
断言的目的是让您知道是否存在错误,不仅在您的代码中,而且在您的代码库中。(这里的断言实际上会标记其他类中的错误。)
即使我的同事确信我们的课程编写得当,这里的断言仍然有用。将添加可能无法测试 null 的新类,并且此方法可以为我们标记这些错误。
在开发中,您应该始终打开断言,即使您编写的代码不使用断言。默认情况下,我的 IDE 设置为始终为任何新的可执行文件执行此操作。
断言不会改变生产中代码的行为,所以我的同事很高兴有空检查,并且即使该
equals()
方法有错误,该方法也将正确执行。我很高兴,因为我会equals()
在开发中发现任何错误的方法。
此外,您应该通过放入一个将失败的临时断言来测试您的断言策略,这样您就可以确定您会通过日志文件或输出流中的堆栈跟踪得到通知。
什么时候应该使用断言?
很多很好的答案解释了assert
关键字的作用,但很少有人回答真正的问题,“什么时候应该assert
在现实生活中使用关键字?” 答案:
几乎从不
断言,作为一个概念,非常棒。好的代码有很多if (...) throw ...
语句(以及它们的亲属,比如Objects.requireNonNull
and Math.addExact
)。但是,某些设计决策极大地限制了assert
关键字本身的效用。
关键字背后的驱动思想assert
是过早优化,主要功能是能够轻松关闭所有检查。事实上,默认情况下assert
检查是关闭的。
但是,在生产中继续进行不变检查至关重要。这是因为完美的测试覆盖率是不可能的,并且所有生产代码都会有错误,断言应该有助于诊断和缓解。
因此,if (...) throw ...
应该首选使用 ,就像检查公共方法的参数值和抛出IllegalArgumentException
.
有时,人们可能会想编写一个不变的检查,该检查确实需要很长时间才能处理(并且经常被调用以使其重要)。但是,这样的检查会减慢测试速度,这也是不可取的。这种耗时的检查通常写成单元测试。assert
然而,出于这个原因,有时使用它可能是有意义的。
不要assert
仅仅因为它比它更干净更漂亮而使用if (...) throw ...
(我很痛苦地说,因为我喜欢干净和漂亮)。如果您无法帮助自己,并且可以控制应用程序的启动方式,那么请随意使用assert
,但始终在生产环境中启用断言。诚然,这是我倾向于做的事情。我正在推动一个 lombok 注释,它将导致assert
行为更像if (...) throw ...
. 在这里投票。
(咆哮:JVM 开发人员是一群糟糕的、过早优化的编码人员。这就是为什么你会听到 Java 插件和 JVM 中有这么多安全问题的原因。他们拒绝在生产代码中包含基本检查和断言,我们将继续付出代价。)
这是最常见的用例。假设您正在打开一个枚举值:
switch (fruit) {
case apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
}
只要你处理好每一个案件,你就没事。但是有一天,有人会将 fig 添加到您的枚举中而忘记将其添加到您的 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;
}
断言用于检查后置条件和“不应该失败”的前置条件。正确的代码不应该使断言失败;当它们触发时,它们应该指出一个错误(希望在一个靠近问题实际位置的地方)。
断言的一个例子可能是检查一组特定的方法是否以正确的顺序被调用(例如,hasNext()
在 之前调用next()
)Iterator
。
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();
}
}
}
}
where Assert.class.desiredAssertionStatus()
is true
when-ea
在命令行上传递,否则为 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
替换,并且.-ea
throw new AssertionError()
断言允许检测代码中的缺陷。您可以打开断言以进行测试和调试,同时在您的程序投入生产时将其关闭。
当你知道它是真的时,为什么要断言它?只有当一切正常时才是正确的。如果程序有缺陷,它实际上可能不是真的。在此过程的早期检测到这一点可以让您知道有问题。
assert
语句包含此语句以及可选消息String
。
assert 语句的语法有两种形式:
assert boolean_expression;
assert boolean_expression: error_message;
以下是一些基本规则,它们管理应该在哪里使用断言以及在哪里不应该使用它们。断言应该用于:
验证私有方法的输入参数。不适用于公共方法。
public
传递错误参数时,方法应抛出常规异常。程序中的任何地方都可以确保几乎可以肯定是正确的事实的有效性。
例如,如果您确定它只会是 1 或 2,则可以使用如下断言:
...
if (i == 1) {
...
}
else if (i == 2) {
...
} else {
assert false : "cannot happen. i is " + i;
}
...
- 在任何方法结束时验证后置条件。这意味着,在执行业务逻辑之后,您可以使用断言来确保变量或结果的内部状态与您的预期一致。例如,打开套接字或文件的方法可以在末尾使用断言来确保确实打开了套接字或文件。
断言不应用于:
验证公共方法的输入参数。由于断言可能并不总是被执行,因此应该使用常规异常机制。
验证对用户输入内容的约束。和上面一样。
不应用于副作用。
例如,这不是一个正确的用法,因为这里的断言用于调用方法的副作用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 文章中的断言)
public int pop() {
// precondition
assert !isEmpty() : "Stack is empty";
return stack[--num];
}
除了此处提供的所有出色答案外,官方的 Java SE 7 编程指南还有一本非常简洁的使用手册assert
;有几个现场示例说明何时使用断言是一个好主意(而且,重要的是,坏主意),以及它与抛出异常有何不同。
Assert在开发时非常有用。如果您的代码正常工作,当某些事情无法发生时,您可以使用它。它易于使用,并且可以永远留在代码中,因为它会在现实生活中被关闭。
如果这种情况有可能在现实生活中发生,那么您必须处理它。
我喜欢它,但不知道如何在 Eclipse/Android/ADT 中打开它。即使在调试时它似乎也关闭了。(这有一个线程,但它指的是'Java vm',它没有出现在ADT运行配置中)。
这是我在服务器中为 Hibernate/SQL 项目编写的断言。一个实体 bean 有两个有效的布尔属性,称为 isActive 和 isDefault。每个都可以有一个值“Y”或“N”或 null,它被视为“N”。我们要确保浏览器客户端仅限于这三个值。因此,在这两个属性的设置器中,我添加了以下断言:
assert new HashSet<String>(Arrays.asList("Y", "N", null)).contains(value) : value;
请注意以下事项。
此断言仅适用于开发阶段。如果客户发送了一个错误的值,我们会在生产之前很久就发现并修复它。断言适用于您可以及早发现的缺陷。
这种断言是缓慢且低效的。没关系。断言可以随意变慢。我们不在乎,因为它们只是开发工具。这不会减慢生产代码,因为断言将被禁用。(在这一点上有一些分歧,我稍后会谈到。)这就引出了我的下一点。
这个断言没有副作用。我本可以针对一个不可修改的静态最终 Set 测试我的值,但该集合会一直存在于生产环境中,永远不会被使用。
存在此断言以验证客户端的正确操作。因此,当我们进入生产阶段时,我们将确保客户端正常运行,因此我们可以安全地关闭断言。
有人问这个问题:如果生产中不需要断言,为什么不在完成后将其取出?因为当您开始开发下一个版本时,您仍然需要它们。
有些人认为你永远不应该使用断言,因为你永远不能确定所有的错误都消失了,所以即使在生产环境中你也需要保留它们。所以使用 assert 语句是没有意义的,因为断言的唯一优点是你可以关闭它们。因此,根据这种想法,您应该(几乎)永远不要使用断言。我不同意。确实,如果测试属于生产环境,则不应使用断言。但是这个测试不属于生产。这个是为了捕捉一个不太可能进入生产环境的错误,所以当你完成后可以安全地关闭它。
顺便说一句,我可以这样写:
assert value == null || value.equals("Y") || value.equals("N") : value;
这仅适用于三个值,但如果可能值的数量变得更大,则 HashSet 版本会变得更方便。我选择了 HashSet 版本来说明我的效率。
断言是可能被关闭的检查。它们很少使用。为什么?
- 它们不能用于检查公共方法参数,因为您无法控制它们。
- 它们不应该用于简单的检查,
result != null
因为这样的检查非常快并且几乎没有什么可以保存的。
那么,还剩下什么?对真正期望为真的条件进行昂贵的检查。一个很好的例子是像 RB-tree 这样的数据结构的不变量。实际上,在JDK8 中,对于.ConcurrentHashMap
TreeNodes
- 你真的不想在生产中打开它们,因为它们很容易支配运行时间。
- 您可能希望在测试期间打开或关闭它们。
- 在处理代码时,您肯定想打开它们。
有时,支票并不是很贵,但同时,你很确定,它会通过。在我的代码中,例如,
assert Sets.newHashSet(userIds).size() == userIds.size();
我很确定我刚刚创建的列表具有独特的元素,但我想记录并仔细检查它。
回顾一下(这对于许多语言来说都是如此,而不仅仅是 Java):
“assert”主要用作软件开发人员在调试过程中的调试辅助。断言消息永远不应该出现。许多语言提供了一个编译时选项,该选项将导致所有“断言”被忽略,用于生成“生产”代码。
“异常”是处理各种错误条件的便捷方式,无论它们是否代表逻辑错误,因为如果遇到无法继续的错误条件,您可以简单地“将它们抛向空中, “无论您身在何处,都希望外面的其他人准备好“抓住”他们。控制是一步转移的,直接从引发异常的代码,直接到捕手的手套。(捕手可以看到已经发生的调用的完整回溯。)
此外,该子例程的调用者不必检查子例程是否成功:“如果我们现在在这里,它一定是成功的,否则它会抛出异常,我们现在不会在这里!” 这种简单的策略使代码设计和调试变得非常容易。
异常方便地允许致命错误条件成为它们的样子:“规则的异常”。而且,让它们由一个代码路径处理,这也是“规则的例外...... ”飞球!
这是另一个例子。我写了一个方法来查找两个排序数组中值的中值。该方法假定数组已经排序。出于性能原因,它不应该首先对数组进行排序,甚至不应该检查以确保它们已排序。但是,使用未排序的数据调用此方法是一个严重的错误,我们希望在开发阶段尽早发现这些错误。所以这就是我如何处理那些看似矛盾的目标:
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) 性能,但“已排序”测试是 order n。所以我把它放在一个断言中,以限制它在开发阶段的使用,我给它起一个名字,让它清楚地表明它不适合生产。
这样我就拥有了两全其美。在开发中,我知道任何错误调用它的方法都会被捕获并修复。而且我知道这样做的缓慢测试不会影响生产中的性能。(这也很好地说明了为什么要在生产中关闭断言,但在开发中打开它们。)
断言基本上用于调试应用程序,或者用于替代某些应用程序的异常处理以检查应用程序的有效性。
断言在运行时起作用。一个简单的例子,可以非常简单地解释整个概念,这里是 - assert 关键字在 Java 中的作用是什么?(维基答案)。
默认情况下禁用断言。要启用它们,我们必须使用-ea
选项运行程序(粒度可以变化)。例如,java -ea AssertionsDemo
。
使用断言有两种格式:
- 简单:例如。
assert 1==2; // This will raise an AssertionError
. - 更好:
assert 1==2: "no way.. 1 is not equal to 2";
这将引发 AssertionError 并显示给定的消息,因此更好。虽然实际语法是assert expr1:expr2
expr2 可以是任何返回值的表达式,但我更经常使用它来打印消息。
基本上,“assert true”会通过,“assert false”会失败。让我们看看这将如何工作:
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;
}
}
assert
是一个关键字。它是在 JDK 1.4 中引入的。有两种类型的assert
s
- 非常简单的
assert
语句 - 简单
assert
的陈述。
默认情况下assert
,不会执行所有语句。如果一个assert
语句接收到 false,那么它将自动引发一个断言错误。