有哪些现实生活中的例子可以帮助我们理解Java assert
关键字的重要作用?
有哪些现实生活中的例子可以帮助我们理解Java assert
关键字的重要作用?
断言(通过assert
关键字)在Java 1.4中添加。它们用于验证代码中的不变量的正确性。它们永远不应该在生产代码中触发,并且表明代码路径的错误或误用。它们可以通过java
命令上的-ea
选项在运行时激活,但默认情况下未启用。
一个例子:
public Foo acquireFoo(int id) {
Foo result = (id > 50) ? fooService.read(id) : new Foo(id);
assert result != null;
return result;
}
assert
来检查公共方法参数(http://docs.oracle.com/javase/1.4.2/docs/guide/lang/assert.html)。这应该抛出一个异常而不是终止程序。 - SJuan76假设你需要编写一个控制核电站的程序。很明显,即使是最小的错误也可能会产生灾难性的后果,因此你的代码必须没有漏洞(为了论证而假设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;
}
如果这个函数出现问题,你会注意到它。你会知道你的代码有问题,你知道它在哪里,以及是什么导致了问题(类似于异常)。更重要的是,当它发生时,你停止执行以防止任何进一步的代码使用错误的值并潜在地损害它所控制的任何内容。new IllegalArgumentException
抛出有什么区别? 除了要在方法声明中添加“throws”和在其他地方处理该异常的代码外,我的意思是。为什么要使用“assert”,而不是抛出新的异常?或者为什么不使用“if”而不是“assert”?我真的搞不清楚:( - Bluerivera
可以是负数,那么检查溢出的断言是错误的。第二个断言是无用的;对于int值,a + b - b == a总是成立的。只有当计算机基本上损坏时,该测试才会失败。为了防范这种情况,需要在多个CPU之间检查一致性。 - kevin cline断言是开发阶段的工具,用于捕获代码中的错误。它们被设计成易于移除,因此在生产代码中不存在。因此,断言不是您交付给客户的“解决方案”的一部分。它们是内部检查,以确保您正在做出正确的假设。最常见的例子是测试 null 值。许多方法都是这样编写的:
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
}
}
这样,您的同事会高兴地看到 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;
}
当thisValue不为空时,这段代码委托equals()
方法的工作。但是它假设equals()
方法正确地履行了处理null参数的equals()
契约。
我的同事对我的代码提出了反对意见,告诉我我们许多类的equals()
方法存在错误,没有测试null,所以我应该在这个方法中加入检查。这是否明智,或者我们应该强制报错,以便我们可以发现并修复它们,这是有争议的,但我听从了我的同事,并添加了一个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()
方法未按其合同要求检查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;
}
记住以下几点:
断言只是开发阶段使用的工具。
断言的作用是让您知道是否存在 bug,不仅在您的代码中,还有在您的 代码库 中。(这里的断言实际上会标记其他类中的错误。)
即使我的同事对我们的类很有信心,这里的断言仍然很有用。新的类可能没有测试 null,而这个方法可以为我们标记这些错误。
在开发过程中,您应该始终打开断言,即使您编写的代码不使用断言。我的 IDE 默认设置为始终这样做以进行任何新的可执行文件。
断言在生产中不改变代码的行为,所以我的同事很高兴空检查在那里,并且即使 equals()
方法有 bug,该方法也将正确执行。我很高兴因为我可以在开发中捕获任何有缺陷的 equals()
方法。
此外,您应该通过放置一个临时断言来测试您的断言策略,以便您确信是否通过日志文件或输出流中的堆栈跟踪通知您失败。
有很多好答案解释了assert
关键字的作用,但很少有人回答真正的问题:"在实际中什么情况下应该使用assert
关键字?" 答案是:
作为一个概念,断言是很棒的。良好的代码有许多if (...) throw ...
语句(及其相关语句如Objects.requireNonNull
和Math.addExact
)。然而,某些设计决策极大地限制了assert
关键字本身的效用。
assert
关键字背后的驱动思想是过早优化,主要特点是能够轻松关闭所有检查。实际上,assert
检查默认是关闭的。
然而,保持不变量检查在生产环境中继续进行非常重要。这是因为完美的测试覆盖率是不可能的,所有的生产代码都会有错误,而断言应该有助于诊断和缓解这些错误。
因此,应该优先使用if (...) throw ...
,就像对于检查公共方法的参数值和抛出IllegalArgumentException
一样需要使用它。
偶尔,你可能会想编写一个不希望花费过长时间来处理的不变量检查(而且被调用的次数足够多,以至于这很重要)。然而,这种检查会减慢测试速度,这也是不希望看到的。这样耗时的检查通常被编写为单元测试。尽管如此,有时候出于这个原因使用assert
也是有意义的。
不要仅仅因为assert
更加简洁美观就使用它,而不是使用if(...) throw ...
(我说这话感到非常痛苦,因为我喜欢代码整洁美观)。如果你真的无法忍受,并且可以控制应用程序的启动方式,那么可以使用assert
,但在生产环境中一定要启用断言。 诚然,这是我倾向于做的。我正在推动一个Lombok注解,使assert
的行为更像if (...) throw ...
。在这里投票支持它。
(抱怨:JVM开发人员是一群可怕的、过早优化的编码者。这也是为什么Java插件和JVM出现了如此多的安全问题的原因。他们拒绝在生产代码中包含基本的检查和断言,我们现在仍在为此付出代价。)
catch (Throwable t)
。没有理由不去尝试捕获、记录或重试/恢复 OutOfMemoryError、AssertionError 等异常。 - Aleksandr Dubinskyassert
关键字的实现很糟糕。我将编辑我的答案,以使表达更清晰,明确我指的是该关键字而不是概念本身。 - Aleksandr DubinskyPreconditions
和Verify
。 断言(在Java中的设计)类似于测试-不应在生产中运行。它们编写起来比测试便宜得多;它们不能取代测试,但它们可以很好地补充测试。 它们可以检查在生产中成本过高的条件。 它们不得被误用为前提条件。 - maaartinus这是最常见的用例。假设您要切换枚举值:
switch (fruit) {
case apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
}
只要你处理每种情况,你就没问题。但总有一天,有人会将 fig 添加到你的枚举中,却忘记在 switch 语句中添加它。这将产生一个错误,可能很难捕获,因为影响直到你离开 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;
}
-ea
),这将抛出一个AssertionError
。在生产中希望的行为是什么?静默无操作并在执行后潜在地造成灾难吗?可能不是。我建议显式地throw new AssertionError("Missing enum value: " + fruit);
。 - aioobedefault
,这样编译器就可以在缺少情况时警告你。你可以使用 return
而不是 break
(这可能需要提取方法),然后在 switch 之后处理缺失的情况。这样你既可以得到警告,也可以有机会进行 assert
。 - maaartinusJava中的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();
}
}
}
}
当命令行传递了-ea
时,Assert.class.desiredAssertionStatus()
返回true
,否则返回false
。
我们使用System.currentTimeMillis()
确保它不会被优化忽略(assert true;
会被优化掉)。
合成字段是为了让Java只需要在加载时调用一次Assert.class.desiredAssertionStatus()
,然后在那里缓存结果。参见: “static synthetic”的含义是什么?
我们可以使用以下方式进行验证:
javac Assert.java
javap -c -constants -private -verbose Assert.class
使用 Oracle JDK 1.8.0_45 版本时,会生成一个合成的静态字段(参见:什么是“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()
.catch (Throwable t)
子句也能捕获断言违规吗?对我来说,这限制了它们的实用性,只适用于断言体耗时的情况,这种情况很少见。 - Evgeni SergeevAssertionError
然后重新抛出它。 - MiguelMunoz断言被用于检查后置条件和“永远不应该失败”的前置条件。正确的代码永远不会触发断言;当它们被触发时,它们应该指示一个错误(希望是在实际问题所在的附近)。
断言的一个例子可能是检查一组特定的方法是否按正确的顺序调用(例如,在Iterator
中调用next()
之前是否先调用了hasNext()
)。
断言允许检测代码中的缺陷。您可以在测试和调试时启用它们,在程序生产中关闭。
当您知道某个情况是正确的时候,为什么还需要进行断言?只有在一切正常工作的情况下才是真实的。如果程序存在缺陷,它可能实际上并不是真实的。在进程的早期检测到这一点可以让您知道出了什么问题。
一个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");
}
以下是来自Stack类的一个实际示例(摘自Java断言文章)
public int pop() {
// precondition
assert !isEmpty() : "Stack is empty";
return stack[--num];
}
assert
;其中有几个恰当的示例,说明何时使用断言是一个好主意(以及重要的是,何时不是),以及它与抛出异常的区别。
链接