有哪些真实的例子来理解断言的关键作用?
当前回答
让我们假设您要编写一个控制核电站的程序。很明显,即使是最微小的错误也可能导致灾难性的结果,因此您的代码必须是无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,成本就越低)。此外,如果代码中有任何错误,您将检测到它。不可能出现漏洞并在以后引起问题。
这仍然不能保证代码没有错误,但它比通常的程序更接近于这一点。
其他回答
断言用于检查后置条件和“永不失败”的前提条件。正确的代码应该永远不会使断言失败;当它们触发时,它们应该指出一个错误(希望是在接近问题的实际位置的地方)。
断言的一个例子可能是检查一组特定的方法是否以正确的顺序被调用(例如,在迭代器中hasNext()在next()之前被调用)。
断言是可以被关闭的检查。它们很少被使用。为什么?
它们不能用于检查公共方法参数,因为您无法控制它们。 它们不应该用于简单的检查,如result != null,因为这样的检查非常快,几乎没有什么可以保存。
那么,还剩下什么呢?昂贵的条件检查是真实的。像rb树这样的数据结构的不变量就是一个很好的例子。实际上,在JDK8的ConcurrentHashMap中,对于TreeNodes有一些这样有意义的断言。
您确实不希望在生产环境中启用它们,因为它们很容易控制运行时间。 您可能希望在测试期间打开或关闭它们。 在处理代码时一定要打开它们。
有时候,支票不是很贵,但与此同时,你很确定,它会通过。在我的代码中,有例如,
assert Sets.newHashSet(userIds).size() == userIds.size();
我很确定我刚刚创建的列表有独特的元素,但我想记录并再次检查它。
让我们假设您要编写一个控制核电站的程序。很明显,即使是最微小的错误也可能导致灾难性的结果,因此您的代码必须是无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,成本就越低)。此外,如果代码中有任何错误,您将检测到它。不可能出现漏洞并在以后引起问题。
这仍然不能保证代码没有错误,但它比通常的程序更接近于这一点。
断言是用于捕获代码中的错误的开发阶段工具。它们被设计为易于删除,因此它们不会存在于生产代码中。因此断言不是您交付给客户的“解决方案”的一部分。它们是内部检查,以确保你所做的假设是正确的。最常见的例子是测试是否为空。很多方法都是这样写的:
void doSomething(Widget widget) {
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
在这样的方法中,小部件通常不应该是空的。所以如果它是空的,在你的代码中有一个bug,你需要追踪。但是上面的代码永远不会告诉你这一点。因此,在编写“安全”代码的善意努力中,您也隐藏了一个错误。这样写代码会更好:
/**
* @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;
}
这段代码在thisValue不为空的情况下委托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时才有必要。
我没有与同事就让有bug的代码留在代码库中是否明智进行毫无结果的争论,而是简单地在代码中放入了两个断言。这些断言会让我知道,在开发阶段,如果我们的一个类不能正确地实现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;
}
需要记住的要点如下:
Assertions are development-phase tools only. The point of an assertion is to let you know if there's a bug, not just in your code, but in your code base. (The assertions here will actually flag bugs in other classes.) Even if my colleague was confident that our classes were properly written, the assertions here would still be useful. New classes will be added that might fail to test for null, and this method can flag those bugs for us. In development, you should always turn assertions on, even if the code you've written doesn't use assertions. My IDE is set to always do this by default for any new executable. The assertions don't change the behavior of the code in production, so my colleague is happy that the null check is there, and that this method will execute properly even if the equals() method is buggy. I'm happy because I will catch any buggy equals() method in development.
此外,您应该通过放入一个将失败的临时断言来测试断言策略,这样您就可以确定通过日志文件或输出流中的堆栈跟踪来通知您。
总结一下(不仅仅是Java,很多语言都是如此):
“assert”主要被软件开发人员在调试过程中用作调试辅助。断言消息永远不会出现。许多语言提供了一个编译时选项,该选项将导致所有“断言”被忽略,用于生成“生产”代码。
"exceptions" are a handy way to handle all kinds of error conditions, whether or not they represent logic errors, because, if you run into an error-condition such that you cannot continue, you can simply "throw them up into the air," from wherever you are, expecting someone else out there to be ready to "catch" them. Control is transferred in one step, straight from the code that threw the exception, straight to the catcher's mitt. (And the catcher can see the complete backtrace of calls that had taken place.)
此外,该子例程的调用者不必检查子例程是否成功:“如果我们现在在这里,它一定成功了,因为否则它会抛出异常,我们现在就不会在这里!”这种简单的策略使得代码设计和调试变得非常非常容易。
异常方便地允许致命错误条件保持原样:“规则的异常”。而且,对于它们来说,由代码路径来处理,这也是“规则的例外……“高飞球!”
推荐文章
- 在流中使用Java 8 foreach循环移动到下一项
- 访问限制:'Application'类型不是API(必需库rt.jar的限制)
- 用Java计算两个日期之间的天数
- 如何配置slf4j-simple
- 在Jar文件中运行类
- 带参数的可运行?
- 我如何得到一个字符串的前n个字符而不检查大小或出界?
- 我可以在Java中设置enum起始值吗?
- Java中的回调函数
- c#和Java中的泛型有什么不同?和模板在c++ ?
- 在Java中,流相对于循环的优势是什么?
- Jersey在未找到InjectionManagerFactory时停止工作
- 在Java流是peek真的只是调试?
- Recyclerview不调用onCreateViewHolder
- 将JSON字符串转换为HashMap