什么是现实生活中的例子,以了解断言的关键作用?
答案 0 :(得分:400)
Assertions(通过 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;
}
答案 1 :(得分:310)
我们假设您应该编写一个程序来控制核电站。很明显,即使是最小的错误也可能带来灾难性的结果,因此您的代码必须无错误(假设JVM在参数方面没有错误)。
Java不是一种可验证的语言,这意味着:您无法计算出您的操作结果是否完美。这个的主要原因是指针:它们可以指向任何地方或任何地方,因此它们不能被计算为具有这个精确值,至少不在合理的代码范围内。鉴于此问题,无法证明您的代码在整体上是正确的。但你可以做的是证明你至少在发生错误时找到它们。
这个想法基于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;
}
虽然这很明显可以正常工作,但是大多数程序员都不会在这个内部看到隐藏的错误(提示:由于类似的错误,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 Exceptions是一个类似的概念,但它们无法验证所有内容。如果您想要更多检查(以执行速度为代价),则需要使用断言。这样做会使您的代码膨胀,但最终您可以在极短的开发时间内交付产品(修复错误越早,成本就越低)。此外:如果代码中有任何错误,您将检测到它。没有办法让错误导致错误并在以后引发问题。
这仍然不能保证无错误的代码,但它比通常的程序更接近它。
答案 2 :(得分:59)
断言是一种用于捕获代码中的错误的开发阶段工具。它们的设计易于删除,因此它们不会存在于生产代码中。因此,断言不是您向客户提供的“解决方案”的一部分。它们是内部检查,以确保您所做的假设是正确的。最常见的例子是测试null。许多方法都是这样写的:
void doSomething(Widget widget) {
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
通常在这样的方法中,小部件应该永远不会为空。因此,如果它为null,那么您的代码中存在一个需要跟踪的错误。但上面的代码永远不会告诉你这一点。因此,为了编写“安全”代码的善意努力,您也隐藏了一个错误。编写这样的代码要好得多:
/**
* @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
}
}
这样,您的同事会很高兴对生产代码进行空检查,但在开发期间,当窗口小部件为空时,您不再隐藏该错误。
这是一个真实的例子:我曾经写过一个方法,比较两个任意值的相等性,其中任何一个值都可以为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()
方法通过正确处理null参数正确地履行了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;
}
仅当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()
方法。
此外,您应该通过放入一个失败的临时断言来测试断言策略,这样您就可以确定通过日志文件或输出流中的堆栈跟踪得到通知。
答案 3 :(得分:19)
很多好的答案解释了assert
关键字的作用,但很少回答真正的问题,"何时应该在现实生活中使用assert
关键字?&#34; < / p>
答案:几乎从未。
断言,作为一个概念,是美好的。好的代码有很多if (...) throw ...
语句(及其亲属,如Objects.requireNonNull
和Math.addExact
)。但是,某些设计决策极大地限制了assert
关键字本身的效用。
assert
关键字背后的驱动理念是过早优化,主要功能是能够轻松关闭所有检查。实际上,默认情况下会关闭assert
检查。
然而,在生产中继续进行不变检查至关重要。这是因为完美的测试覆盖是不可能的,并且所有生产代码都会有错误,断言应该有助于诊断和缓解。
因此,应优先使用if (...) throw ...
,就像检查公共方法的参数值和投掷IllegalArgumentException
所需的一样。
有时候,人们可能会想要编写一个不变的检查,这个检查确实需要花费不必要的长时间来处理(并且通常被称为足够重要)。但是,这种检查会减慢测试速度,这也是不合需要的。这种耗时的检查通常写成单元测试。然而,出于这个原因,有时使用assert
是有意义的。
不要仅仅使用assert
,因为它比if (...) throw ...
更清洁,更漂亮(我说的很痛苦,因为我喜欢干净漂亮)。如果您无法自助,并且可以控制应用程序的启动方式,那么可以随意使用assert
,但始终在生产中启用断言。不可否认,这就是我倾向于做的事情。我正在推动一个lombok注释,这将使assert
更像if (...) throw ...
。 Vote for it here.
(Rant:JVM开发人员是一群可怕的,过早优化的编码器。这就是为什么你在Java插件和JVM中听到很多安全问题的原因。他们拒绝在生产代码中包含基本检查和断言,而我们正在继续付出代价。)
答案 4 :(得分:13)
这是最常见的用例。假设您正在启用枚举值:
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;
}
答案 5 :(得分:12)
断言用于检查后置条件和“永远不会失败”的前置条件。正确的代码永远不会失败一个断言;当它们触发时,它们应该指出一个错误(希望在一个接近问题实际位置的地方)。
断言的一个示例可能是检查是否以正确的顺序调用了一组特定的方法(例如,在hasNext()
中next()
之前调用了Iterator
。
答案 6 :(得分:7)
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
,否则为false。
我们使用System.currentTimeMillis()
来确保它不会被优化掉(assert true;
已经完成)。
生成合成字段,以便Java只需在加载时调用Assert.class.desiredAssertionStatus()
一次,然后将结果缓存在那里。另见:What is the meaning of "static synthetic"?
我们可以通过以下方式验证:
javac Assert.java
javap -c -constants -private -verbose Assert.class
使用Oracle JDK 1.8.0_45,生成了一个合成静态字段(另请参阅:What is the meaning of "static synthetic"?):
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()
。答案 7 :(得分:6)
断言允许检测代码中的缺陷。您可以打开断言进行测试和调试,同时在程序投入生产时将其关闭。
为什么当你知道它是真的时断言?只有当一切正常时,才会这样。如果程序存在缺陷,则可能实际上并非如此。在此过程的早期检测到这一点可以让您知道出现了问题。
assert
语句包含此语句以及可选的String
消息。
assert语句的语法有两种形式:
assert boolean_expression;
assert boolean_expression: error_message;
以下是一些基本规则,用于管理应使用断言的位置以及不应使用断言的位置。断言应用于:
验证私有方法的输入参数。 NOT 用于公共方法。传递错误参数时,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");
}
答案 8 :(得分:6)
一个真实世界的例子,来自Stack-class(来自Assertion in Java Articles)
public int pop() {
// precondition
assert !isEmpty() : "Stack is empty";
return stack[--num];
}
答案 9 :(得分:5)
除了这里提供的所有优秀答案之外,官方Java SE 7编程指南还有一个关于使用assert
的简明手册;有几个现场例子说明何时使用断言是一个好的(而且是重要的,坏的)想法,以及它与抛出异常的方式有什么不同。
答案 10 :(得分:3)
断言非常非常有用。如果代码工作正常,当无法发生某些事情时,就可以使用它。它易于使用,并且可以永久保留在代码中,因为它将在现实生活中被关闭。
如果在现实生活中有可能发生这种情况,那么你必须处理它。
我喜欢它,但不知道如何在Eclipse / Android / ADT中打开它。即使在调试时它似乎也已关闭。 (这里有一个线程,但它指的是'Java vm',它没有出现在ADT运行配置中。)
答案 11 :(得分:3)
这是我在服务器中为Hibernate / SQL项目编写的断言。实体bean有两个有效布尔属性,称为isActive和isDefault。每个人都可以拥有&#34; Y&#34;或&#34; N&#34;或null,被视为&#34; N&#34;。我们希望确保浏览器客户端仅限于这三个值。所以,在我对这两个属性的setter中,我添加了这个断言:
assert new HashSet<String>(Arrays.asList("Y", "N", null)).contains(value) : value;
请注意以下事项。
此断言仅适用于开发阶段。如果客户发送了一个错误的价值,我们会在我们达到生产之前很早就抓住它并修复它。断言是指您可以及早发现的缺陷。
这个断言很慢且效率低下。没关系。断言可以自由地缓慢。我们不关心,因为他们只是开发工具。这不会减慢生产代码的速度,因为断言将被禁用。 (在这一点上存在一些分歧,我稍后会谈到这一点。)这导致了我的下一步。
这个断言没有副作用。我本可以针对一个不可修改的静态最终版本测试我的价值,但是这个版本在生产中会保持不变,永远不会被使用。
此断言用于验证客户端的正常运行。因此,当我们到达生产时,我们将确保客户端正常运行,因此我们可以安全地关闭断言。
有人问这个问题:如果生产中不需要断言,为什么不在你做完之后把它们拿出来?因为当您开始处理下一个版本时,您仍然需要它们。
有些人认为你永远不应该使用断言,因为你永远不能确定所有的bug都消失了,所以你需要在生产中保留它们。所以使用assert语句没有任何意义,因为断言的唯一优点是你可以关闭它们。因此,根据这种想法,你应该(几乎)从不使用断言。我不同意。如果测试属于生产,那么你不应该使用断言。但是这个测试不属于生产。这个是为了捕捉一个不太可能达到生产的错误,所以当你完成它时可以安全地关闭它。
顺便说一句,我本可以这样写的:assert value == null || value.equals("Y") || value.equals("N") : value;
这仅适用于三个值,但如果可能值的数量变大,则HashSet版本变得更加方便。我选择HashSet版本来表达我对效率的看法。
答案 12 :(得分:2)
默认情况下禁用断言。要启用它们,我们必须使用canceledit
选项运行程序(粒度可以变化)。例如,listeners: (function(){
var editing;
return {
beforeselect: function(){
return !editing;
},
beforeedit: function(){
editing = true;
},
edit: function(){
editing = false;
},
canceledit: function(){
editing = false;
}
}
}())
。
使用断言有两种格式:
-ea
。java -ea AssertionsDemo
这将引发AssertionError,并显示给定的消息,因此更好。虽然实际语法是assert 1==2; // This will raise an AssertionError
,其中expr2可以是返回值的任何表达式,但我更常用它来打印消息。答案 13 :(得分:2)
断言主要用于调试应用程序,或者用于替换某些应用程序的异常处理,以检查应用程序的有效性。
断言在运行时工作。一个简单的例子,可以很简单地解释整个概念,在这里 - What does the assert keyword do in Java? (WikiAnswers)。
答案 14 :(得分:1)
回顾(对于许多语言而言,这不仅仅是Java):
“assert”主要用作软件开发人员在调试过程中的调试辅助工具。永远不会出现断言消息。许多语言都提供了一个编译时选项,可以忽略所有“断言”,用于生成“生产”代码。
“exception”是一种处理各种错误条件的便捷方式,无论它们是否代表逻辑错误,因为如果遇到错误条件而无法继续,你可以简单地“把它们放入空气,“无论你身在何处,都希望其他人准备好”抓住“他们。控制转移一步,直接从抛出异常的代码转移到捕手的手套。 (捕手可以看到已经发生的电话的完整回溯。)
此外,该子例程的调用者不必检查子例程是否成功:“如果我们现在在这里,它一定是成功的,因为否则它会抛出异常而我们不会'现在就在这里!“这个简单的策略使代码设计和调试变得更加容易。
异常方便地允许致命错误条件成为它们:“规则的例外情况。”而且,对于他们来说,这是一个代码路径,也是“规则的例外...... ”飞球!“
答案 15 :(得分:0)
断言是可能会关闭的检查。他们很少使用。为什么?
result != null
之类的简单检查,因为此类检查非常快并且几乎没有什么可保存的。那么,还剩下什么? 昂贵检查条件,期望确实为真。一个很好的例子是像RB-tree这样的数据结构的不变性。实际上,在JDK8的ConcurrentHashMap
中,对于TreeNodes
有一些有意义的断言。
答案 16 :(得分:0)
这是另一个例子。我写了一种方法,可以找到两个排序数组中的值的中位数。该方法假定数组已经排序。出于性能原因,它不应该首先对数组进行排序,甚至不要检查以确保对数组进行了排序。但是,使用未排序的数据调用此方法是一个严重的错误,我们希望在开发阶段尽早发现这些错误。因此,这就是我如何处理那些看似相互矛盾的目标:
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)性能,但“已排序”测试的顺序为n。因此,我将其放入一个断言中,以将其限制在开发阶段,并为其命名,以表明它不适合用于生产。
这样,我可以两全其美。在开发中,我知道任何错误地调用此方法的方法都会被捕获并修复。而且我知道这样做的缓慢测试不会影响生产性能。 (这也很好地说明了为什么要在生产中保留断言,而在开发中将其打开。)
答案 17 :(得分:0)
基本上,“断言真”将通过,“断言假”将失败。让我们来看看它是如何工作的:
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;
}
}
答案 18 :(得分:-8)
assert
是一个关键字。它是在JDK 1.4中引入的。有两种类型的assert
s
assert
陈述assert
陈述。默认情况下,不会执行所有assert
语句。如果assert
语句收到false,则会自动引发断言错误。