有哪些现实的例子可以理解断言的关键作用?
有哪些现实的例子可以理解断言的关键作用?
Answers:
断言(通过assert关键字)是在Java 1.4中添加的。它们用于验证代码中不变式的正确性。切勿在生产代码中触发它们,并表示存在错误或滥用代码路径。可以在运行时通过命令-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;
}
assert
来检查公共方法参数(docs.oracle.com/javase/1.4.2/docs/guide/lang/assert.html)。那应该抛出一个Exception
而不是杀死程序。
This convention is unaffected by the addition of the assert construct. Do not use assertions to check the parameters of a public method. An assert is inappropriate because the method guarantees that it will always enforce the argument checks. It must check its arguments whether or not assertions are enabled. Further, the assert construct does not throw an exception of the specified type. It can throw only an AssertionError.
docs.oracle.com/javase/8/docs/technotes/guides/language/…–
假设您应该编写一个程序来控制核电站。很明显,即使是最轻微的错误也可能导致灾难性的结果,因此您的代码必须没有错误(假设出于参数考虑,JVM是没有错误的)。
Java不是可验证的语言,这意味着:您无法计算出操作结果是否完美。造成这种情况的主要原因是指针:它们可以指向任何地方或任何地方,因此无法计算出它们的确切值,至少不在合理的代码范围内。鉴于此问题,没有办法证明您的代码总体上是正确的。但是您可以做的是证明您至少在发现每个错误时都会发现它。
这个想法是基于合同设计(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;
}
如果此功能现在失败,您将注意到它。您将知道代码中存在问题,知道问题出在哪里,并且知道是什么原因引起的(类似于Exceptions)。更重要的是:您会在发生错误时停止正确执行,以防止任何其他代码使用错误的值工作,并可能损坏其控制的内容。
Java异常是一个类似的概念,但是它们无法验证所有内容。如果要进行更多检查(以执行速度为代价),则需要使用断言。这样做会使您的代码膨胀,但是最终您可以在出乎意料的短开发时间内交付产品(越早修复错误,成本越低)。另外:如果您的代码中有任何错误,您将对其进行检测。没有任何漏洞可以通过,并在以后引起问题。
这仍然不能保证没有错误的代码,但是比通常的程序更接近于此。
new IllegalArgumentException
消息中抛出a 和有什么区别?我的意思是,除了throws
要在方法声明和代码中添加o 来管理其他地方的异常。为什么要assert
抛出新的异常?还是为什么不使用if
代替assert
?真的不能得到这个:(
a
可以为负,则检查溢出的断言是错误的。第二个断言是没有用的。对于int值,总是+ a-b-b == a。仅当计算机从根本上损坏时,该测试才会失败。为了避免这种情况的发生,您需要检查多个CPU之间的一致性。
断言是一个开发阶段的工具,可以捕获代码中的错误。它们被设计为易于删除,因此在生产代码中将不存在。因此,断言不是您交付给客户的“解决方案”的一部分。它们是内部检查,以确保您所做的假设正确。最常见的示例是测试null。许多方法是这样写的:
void doSomething(Widget widget) {
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
通常,在这样的方法中,小部件绝对不应为null。因此,如果它为null,则您的代码中存在一个需要跟踪的错误。但是上面的代码永远不会告诉您这一点。因此,在精心编写“安全”代码的过程中,您还隐藏了一个错误。编写这样的代码会更好:
/**
* @param Widget widget Should never be null
*/
void doSomething(Widget widget) {
assert widget != null;
widget.someMethod(); // ...
... // do more stuff with this widget
}
这样,您一定会早日发现此错误。(在合同中指定此参数永远不应为null也很有用。)在开发过程中测试代码时,请确保打开断言。(而且说服您的同事也经常这样做很困难,我觉得这很烦人。)
现在,您的一些同事将反对此代码,认为您仍应进行null检查以防止生产中出现异常。在那种情况下,断言仍然有用。您可以这样写:
void doSomething(Widget widget) {
assert widget != null;
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
这样,您的同事会很高兴为生产代码提供了null检查,但是在开发过程中,当widget为null时,您不再隐藏错误。
这是一个真实的示例:我曾经编写一种方法,比较两个任意值的相等性,其中两个值都可以为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;
}
equals()
如果thisValue不为null ,则此代码委派方法的工作。但是它假定该equals()
方法equals()
通过正确处理null参数正确履行了契约。
一位同事反对我的代码,告诉我我们的许多类都具有equals()
无法测试null的错误方法,因此我应该将该检查放入该方法中。这是否明智还是值得商bat的,还是应该强制执行此错误,以便我们找出并修复该错误,但是我推迟到同事那里并进行了空检查,并在上面加上了注释:
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;
}
要牢记的重点是:
断言仅是开发阶段的工具。
断言的目的是让您不仅在代码中,而且在代码库中都知道是否存在错误。(这里的断言实际上会标记其他类中的错误。)
即使我的同事相信我们的课堂写作正确,这里的主张仍然有用。将添加可能无法测试null的新类,并且该方法可以为我们标记这些错误。
在开发中,即使编写的代码未使用断言,也应始终打开断言。默认情况下,我的IDE设置为始终对任何新的可执行文件执行此操作。
这些断言不会改变生产中代码的行为,因此我的同事很高兴存在null检查,并且即使该equals()
方法有错误,该方法也将正确执行。我很高兴,因为我会equals()
在开发中发现任何错误的方法。
另外,您应该通过放置将失败的临时断言来测试断言策略,以便可以确定是通过日志文件还是通过输出流中的堆栈跟踪来通知您。
很多很好的答案说明了assert
关键字的作用,但是很少回答真正的问题:“何时assert
在现实生活中使用关键字?”
答案:几乎永远不会。
断言作为一个概念是很棒的。好的代码包含很多if (...) throw ...
语句(它们的亲戚喜欢Objects.requireNonNull
和Math.addExact
)。但是,某些设计决策极大地限制了assert
关键字本身的用途。
assert
关键字背后的驱动思想是过早的优化,其主要功能是能够轻松关闭所有检查。实际上,assert
检查默认情况下处于关闭状态。
但是,至关重要的是在生产中继续进行不变检查。这是因为不可能实现完美的测试覆盖,并且所有生产代码都将包含错误,而断言应有助于诊断和缓解错误。
因此,if (...) throw ...
应该首选使用,就像检查公共方法的参数值和throwing一样IllegalArgumentException
。
有时,人们可能会倾向于编写不变的检查,这确实需要花费很长的时间来处理(并且经常被调用以使其变得重要)。但是,这样的检查会使测试变慢,这也是不希望的。这种耗时的检查通常被编写为单元测试。尽管如此,assert
出于这个原因有时使用还是有意义的。
不要assert
仅仅因为它比它更干净,更漂亮而使用它if (...) throw ...
(我说这很痛苦,因为我喜欢干净漂亮)。如果您不能自已,并且可以控制应用程序的启动方式,则可以随时使用,assert
但始终在生产中启用断言。诚然,这是我倾向于做的。我正在推动龙目岛注释,这将使assert
行为更像if (...) throw ...
。在这里投票。
(Rant:JVM开发人员是一堆糟糕的,过早优化的编码器。这就是为什么您听说Java插件和JVM中存在如此多的安全性问题的原因。他们拒绝在生产代码中包含基本的检查和断言,并且我们将继续付出代价。)
catch (Throwable t)
。我们没有理由不尝试陷阱,日志,或重试/从OutOfMemoryError异常,Asse田,等恢复
assert
关键字不好。我将对答案进行编辑,以使其更清楚地表明我所指的是关键字,而不是概念。
这是最常见的用例。假设您要打开一个枚举值:
switch (fruit) {
case apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
}
只要您处理好每种情况,就可以了。但是总有一天,有人会将无花果添加到您的枚举中,而忘记将其添加到您的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;
}
AssertionError
如果启用了断言,则会抛出一个(-ea
)。生产中期望的行为是什么?在执行过程中无声的无操作和潜在的灾难?可能不是。我建议一个明确的throw new AssertionError("Missing enum value: " + fruit);
。
default
以便编译器可以在遗失案件时向您发出警告。您可以return
代替break
(这可能需要提取方法),然后在切换后处理丢失的情况。这样,您既可以得到警告,也可以得到机会assert
。
断言用于检查后置条件和“永不失败”的先决条件。正确的代码永远都不能使断言失败。当它们触发时,它们应该指出一个错误(希望是在一个问题的实际位置附近的地方)。
断言的一个示例可能是检查是否以正确的顺序调用了一组特定的方法(例如,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();
}
}
}
}
其中Assert.class.desiredAssertionStatus()
是true
当-ea
传递在命令行上,否则为假。
我们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()
。catch (Throwable t)
子句也能够捕获断言吗?对我来说,将它们的效用限制在断言主体非常耗时的情况下,这种情况很少见。
AssertionError
一个并重新抛出它。
来自Stack类的真实示例(来自Java文章中的Assertion)
public int pop() {
// precondition
assert !isEmpty() : "Stack is empty";
return stack[--num];
}
断言允许检测代码中的缺陷。您可以打开断言进行测试和调试,而在程序投入生产时将其关闭。
当您知道某件事是真实的时,为什么要断言?只有当一切正常工作时,这才是正确的。如果程序有缺陷,则可能实际上并非如此。在过程的较早阶段检测到这一点,可以让您知道出了点问题。
一个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 SE 7编程指南还提供了一个非常简洁的使用手册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;
请注意以下内容。
此声明仅用于开发阶段。如果客户发送了一个差值,我们将在生产之前很早就发现并修复它。断言是针对您可以及早发现的缺陷。
该断言是缓慢且低效的。没关系。断言可以放慢速度。我们不在乎,因为它们只是开发工具。因为断言将被禁用,所以这不会减慢生产代码的速度。(在这一点上有一些分歧,我将在后面讨论。)这引出了我的下一点。
该断言没有副作用。我本可以针对不可修改的静态最终集测试我的价值,但是该集会一直存在于生产环境中,永远不会被使用。
存在此断言以验证客户端的正确操作。因此,到生产时,我们将确保客户端运行正常,因此可以安全地关闭断言。
有人问:如果生产中不需要断言,为什么不干完就把它们删除呢?因为当您开始使用下一个版本时,仍然需要它们。
有人认为您永远不要使用断言,因为您永远不能确保所有的bug都消失了,因此即使在生产中也需要保持它们不变。因此,使用assert语句没有任何意义,因为asserts的唯一优点是可以将其关闭。因此,根据这种想法,您(几乎)不应使用断言。我不同意。当然,如果测试属于生产环境,则不应使用断言。但这种测试并没有在生产中属于。这是为了捕获一个不可能实现生产的错误,因此完成后可以安全地将其关闭。
顺便说一句,我可以这样写:
assert value == null || value.equals("Y") || value.equals("N") : value;
这仅适用于三个值,但是如果可能值的数量变大,则HashSet版本将变得更加方便。我选择了HashSet版本来说明效率。
HashSet
会带来比s更快的速度优势ArrayList
。此外,集合和列表创建支配着查找时间。使用常量时它们很好。都说,+ 1。
断言基本上用于调试应用程序,或者用于代替某些应用程序的异常处理以检查应用程序的有效性。
断言在运行时起作用。这里有一个简单的示例,可以非常简单地解释整个概念:Java中的assert关键字做什么?(WikiAnswers)。
断言默认为禁用。要启用它们,我们必须使用-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可以是返回值的任何表达式的地方,但我经常使用它来打印消息。回顾一下(这对许多语言都是正确的,而不仅仅是Java):
“断言”主要在调试过程中被软件开发人员用作调试辅助工具。断言消息永远都不会出现。许多语言提供了一个编译时选项,该选项将导致所有“断言”被忽略,以用于生成“生产”代码。
无论您是否表示逻辑错误,“异常”都是一种处理各种错误情况的便捷方法,因为如果遇到了无法继续的错误情况,您可以简单地“将它们抛诸脑后,无论您身在何处,都希望其他人能随时“抓住”他们。控制权直接从引发异常的代码直接转移到捕手的手套,只需一步即可。(捕获器可以看到已发生的呼叫的完整回溯。)
而且,该子例程的调用者不必检查该子例程是否成功:“如果我们现在在这里,则它一定已经成功,因为否则它将引发异常,因此我们现在就不会在这里!” 这种简单的策略使代码设计和调试变得非常容易。
异常可以很方便地使致命错误条件成为它们:“规则例外”。而且,对于它们来说,它们也应由代码路径来处理,该路径也是“规则的例外…… 飞球!”
断言是可能会关闭的检查。他们很少使用。为什么?
result != null
因为这样的检查非常快,几乎没有什么可保存的。那么,还剩下什么呢?贵为条件检查真的希望是真的。一个很好的例子是像RB-tree这样的数据结构的不变性。实际上,在ConcurrentHashMap
JDK8中,对于来说有一些有意义的断言TreeNodes
。
有时,支票并不是真的很贵,但同时,您肯定可以通过。在我的代码中,例如
assert Sets.newHashSet(userIds).size() == userIds.size();
我敢肯定,我刚刚创建的列表具有唯一的元素,但是我想对其进行记录并再次检查。
assert
是一个关键字。它是在JDK 1.4中引入的。该两种类型的assert
小号
assert
陈述assert
陈述。默认情况下,assert
将不执行所有语句。如果一条assert
语句接收到错误,那么它将自动引发一个断言错误。