Java断言关键字做了什么,何时应该使用它?

Pra*_*een 581 java assertions

有哪些实际例子可以理解断言的关键作用?

Oph*_*ian 417

在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;
}
Run Code Online (Sandbox Code Playgroud)

  • 实际上,Oracle告诉您不要使用`assert`来检查公共方法参数(http://docs.oracle.com/javase/1.4.2/docs/guide/lang/assert.html).这应该抛出一个`Exception`而不是杀死程序. (68认同)
  • 但你仍然不解释它们存在的原因.为什么不能进行if()检查并抛出异常? (9认同)
  • `这个约定不受添加断言构造的影响.不要使用断言来检查公共方法的参数.断言是不合适的,因为该方法保证它将始终强制执行参数检查.它必须检查其参数是否启用断言.此外,assert构造不会抛出指定类型的异常.它只能抛出AssertionError.https://docs.oracle.com/javase/8/docs/technotes/guides/language/assert.html (8认同)
  • @ElMac - 断言用于循环的开发/调试/测试部分 - 它们不用于生产.一个if块在prod中运行.简单的断言不会破坏银行,但是进行复杂数据验证的昂贵断言可能会降低您的生产环境,这就是为什么它们被关闭的原因. (6认同)
  • @hoodaticus你的意思是我可以打开/关闭prod代码的所有断言是什么原因?因为我可以进行复杂的数据验证,然后处理异常.如果我有生产代码,我可以关闭复杂(也许是昂贵的)断言,因为它应该工作并且已经过测试了吗?理论上他们不应该打倒这个程序,因为那样你就会遇到问题. (2认同)

Two*_*The 320

让我们假设您应该编写一个程序来控制核电站.很明显,即使是最小的错误也可能带来灾难性的结果,因此您的代码必须没有错误(假设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;
}
Run Code Online (Sandbox Code Playgroud)

虽然这很明显可以正常工作,但大多数程序员都不会在这个内部看到隐藏的错误(提示:由于类似的错误,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;
}
Run Code Online (Sandbox Code Playgroud)

如果此功能现在失败,您会注意到它.您将知道代码中存在问题,您知道它在哪里并且您知道是什么导致它(类似于异常).更重要的是:当它碰巧阻止任何进一步的代码使用错误的值并且可能对其控制的任何内容造成损害时,你就会停止执行.

Java Exceptions是一个类似的概念,但它们无法验证所有内容.如果您想要更多检查(以执行速度为代价),则需要使用断言.这样做会使您的代码膨胀,但最终您可以在极短的开发时间内交付产品(修复错误越早,成本就越低).此外:如果代码中有任何错误,您将检测到它.没有办法让bug滑倒并在以后导致问题.

这仍然不是无错误代码的保证,但它比通常的程序更接近这一点.

  • 我选择这个例子是因为它在看似无错误的代码中提供了隐藏的错误.如果这与其他人提出的相似,那么他们可能会有同样的想法.;) (29认同)
  • -1:如果`a`可以是负数,则检查溢出的断言是错误的.第二个断言没用; 对于int值,始终是a + b - b == a.如果计算机从根本上被破坏,该测试只会失败.为了抵御这种意外情况,您需要检查多个CPU之间的一致性. (13认同)
  • 您选择assert是因为断言为false时失败.一个if可以有任何行为.击中边缘情况是单元测试的工作.使用合同设计指定合同相当好,但与现实生活合同一样,您需要一个控制以确保它们得到尊重.有了断言,就会插入一个看门狗,当合同被尊重时,你就可以了.可以把它想象成一个唠叨的律师,每当你做一些外面的事情或违反你签署的合同时尖叫"错误",然后把你送回家,这样你就无法继续工作并进一步违反合同! (6认同)
  • 在这个简单的例子中是必要的:不,但是DbC定义必须检查__every__结果.想象一下,有人现在将该功能修改为更复杂的功能,然后他也必须调整后检查,然后突然变得有用. (5认同)
  • 抱歉复活了这个,但我有一个具体的问题.什么是@TwoThe所做的,而不是使用断言只是抛出一个新的IllegalArgumentException与消息?我的意思是,除了在方法声明中添加`throws`和在其他地方管理该异常的代码.为什么`断言'引发新的异常?或者为什么不是`if`而不是`assert`?真的不能得到这个:( (4认同)
  • 我仍然不明白为什么选择使用 `assert` 而不是仅仅使用一些 `if` 条件?要触发这些“断言”,您必须在测试时遇到这些边缘情况,但这并不总是发生。那么在忽略“assert”的生产环境中会发生什么? (2认同)
  • 代码中可能存在一个"try {} catch(Throwable t)",它会隐藏这些异常.并且你可以做`if(...)`(在某些语言中,断言甚至是这样编码的),但你在错误的情况下做了什么?你的if只会复制断言正在做什么,或者你会降低它们的效果.断言的一个目的是他们捅你的眼睛并尖叫"FIX ME !!" 直到你这样做.可以说,断言和懒惰. (2认同)
  • 可以用数学精度验证会更准确。计算机科学的整个领域都试图弄清楚您不仅可以假设而且可以(在编译时)证明您的程序正在执行应做的事情(例如,请参见Ada Spark)。对于到达范围有限的小段代码,这很容易做到,但是一旦您引入了全局变量,或更糟糕的是:全局变量不仅包含一个值,而且指向具有多个值的对象,或者甚至指向多个其他指针,好吧,整个结构变得如此复杂,以至于任务变得难以管理。 (2认同)

Mig*_*noz 62

断言是一种用于捕获代码中的错误的开发阶段工具.它们的设计易于删除,因此它们不会存在于生产代码中.因此,断言不是您向客户提供的"解决方案"的一部分.它们是内部检查,以确保您所做的假设是正确的.最常见的例子是测试null.许多方法都是这样写的:

void doSomething(Widget widget) {
  if (widget != null) {
    widget.someMethod(); // ...
    ... // do more stuff with this widget
  }
}
Run Code Online (Sandbox Code Playgroud)

通常在这样的方法中,小部件应该永远不会为空.因此,如果它为null,那么您的代码中存在一个需要跟踪的错误.但上面的代码永远不会告诉你这一点.因此,为了编写"安全"代码的善意努力,您也隐藏了一个错误.编写这样的代码要好得多:

/**
 * @param Widget widget Should never be null
 */
void doSomething(Widget widget) {
  assert widget != null;
  widget.someMethod(); // ...
    ... // do more stuff with this widget
}
Run Code Online (Sandbox Code Playgroud)

这样,您一定会尽早发现这个错误.(在合同中指定此参数永远不应为null也很有用.)确保在开发期间测试代码时打开断言.(说服你的同事这样做也很困难,我觉得很烦人.)

现在,你的一些同事会反对这段代码,认为你仍然应该进行空检查以防止生产中的异常.在这种情况下,断言仍然有用.你可以像这样写:

void doSomething(Widget widget) {
  assert widget != null;
  if (widget != null) {
    widget.someMethod(); // ...
    ... // do more stuff with this widget
  }
}
Run Code Online (Sandbox Code Playgroud)

这样,您的同事会很高兴对生产代码进行空检查,但在开发期间,当窗口小部件为空时,您不再隐藏该错误.

这是一个真实的例子:我曾经写过一个方法,比较两个任意值的相等性,其中任何一个值都可以为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;
}
Run Code Online (Sandbox Code Playgroud)

此代码equals()在thisValue不为null的情况下委托方法的工作.但它假设该equals()方法equals()通过正确处理null参数正确地履行了契约.

一位同事反对我的代码,告诉我许多类都有错误的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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

要记住的重点是:

  1. 断言只是开发阶段工具.

  2. 断言的目的是让你知道是否存在错误,不只是在你的代码中,而是在你的代码库中.(这里的断言实际上会标记其他类中的错误.)

  3. 即使我的同事确信我们的课程写得很好,这里的断言仍然有用.将添加可能无法测试null的新类,并且此方法可以为我们标记这些错误.

  4. 在开发过程中,即使您编写的代码不使用断言,也应始终打开断言.我的IDE设置为默认为任何新的可执行文件执行此操作.

  5. 断言不会改变生产中代码的行为,所以我的同事很高兴有空检查,并且即使方法有问题,这个方法也会正确执行equals().我很高兴因为我会equals()在开发中捕获任何错误的方法.

此外,您应该通过放入一个失败的临时断言来测试断言策略,这样您就可以确定通过日志文件或输出流中的堆栈跟踪得到通知.


Ale*_*sky 17

很多好的答案解释了assert关键字的作用,但很少回答真正的问题,"什么时候应该assert在现实生活中使用关键字?"

答案:几乎从不.

断言,作为一个概念,是美好的.良好的代码有很多的if (...) throw ...陈述(和他们的亲属象Objects.requireNonNullMath.addExact).但是,某些设计决策极大地限制了assert 关键字本身的效用.

assert关键字背后的驱动思想是过早优化,主要功能是能够轻松关闭所有检查.实际上,assert默认情况下会关闭检查.

但是,在生产中继续进行不变检查至关重要.这是因为完美的测试覆盖是不可能的,并且所有生产代码都会有错误,断言应该有助于诊断和缓解.

因此,if (...) throw ...应该首选使用,就像检查公共方法的参数值和抛出所需的一样IllegalArgumentException.

偶尔,人们可能会想要编写一个不变的检查,这个检查确实需要花费不必要的长时间来处理(并且通常被称为足够重要).但是,这种检查会减慢测试速度,这也是不合需要的.这种耗时的检查通常写成单元测试.然而,assert出于这个原因,有时使用它可能是有意义的.

不要assert仅仅因为它比它更清洁和更漂亮而使用if (...) throw ...(并且我说它非常痛苦,因为我喜欢干净漂亮).如果您无法自助,并且可以控制应用程序的启动方式,那么请随意使用,assert始终在生产中启用断言.不可否认,这是我倾向于做的事情.我正在推动一个lombok注释,它将导致assert更像if (...) throw ....在这里投票.

(Rant:JVM开发人员是一群可怕的,过早优化的编码器.这就是你在Java插件和JVM中听到如此多安全问题的原因.他们拒绝在生产代码中包含基本检查和断言,我们继续付出代价.)

  • @aberglas一个catch-all子句是`catch(Throwable t)`.没有理由不尝试从OutOfMemoryError,AssertionError等中捕获,记录或重试/恢复. (2认同)
  • @MiguelMunoz在我的回答中,我说过断言的想法非常好.它是`assert`关键字的实现是坏的.我将编辑我的答案,以便更清楚地表明我指的是关键字,而不是概念. (2认同)
  • 我喜欢它抛出AssertionError而不是Exception的事实。如果代码只能抛出类似IOException之类的东西,仍然有太多的开发人员还不了解他们不应该捕获Exception。我的代码中的错误被完全吞噬,因为有人捕获了Exception。断言不会陷入此陷阱。例外是您希望在生产代码中看到的情况。至于记录,即使错误很少见,您也应该记录所有错误。例如,您真的要让OutOfMemoryError传递而不记录它吗? (2认同)
  • 我强烈不同意。你希望断言是别的东西,但 Guava 中已经有“先决条件”和“验证”等等。断言(如 Java 中设计的)是一种测试 - 不在生产中运行。编写它们比测试便宜得多;它们不能取代测试,但可以很好地补充测试。他们可以检查在生产中检查成本太高而无法检查的条件。它们不得被滥用作为先决条件。 (2认同)

Mig*_*noz 13

这是最常见的用例.假设您正在启用枚举值:

switch (fruit) {
  case apple:
    // do something
    break;
  case pear:
    // do something
    break;
  case banana:
    // do something
    break;
}
Run Code Online (Sandbox Code Playgroud)

只要你处理每一个案子,你都没事.但总有一天,有人会在你的枚举中添加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;
}
Run Code Online (Sandbox Code Playgroud)

  • 为什么在这里使用断言而不是某种例外,例如,一个非法的例子? (9认同)
  • 这就是为什么你应该启用警告并将警告视为错误.任何中途正常的编译器都能告诉你,如果只是你允许它告诉你,你缺少枚举检查,它将在编译时这样做,这比(或许,有一天)发现的更难以理解运行. (4认同)
  • 如果启用了断言(`-ea`),这将抛出一个`AssertionError`.生产中期望的行为是什么?执行后期的无声无操作和潜在灾难?可能不是.我建议一个明确的`throw new AssertionError("Missing enum value:"+ fruit);`. (4认同)
  • 对于仅抛出断言错误,有一个很好的论据。至于生产中的正确行为,断言的全部目的是防止这种情况在生产中发生。断言是一种用于捕获错误的开发阶段工具,可以轻松地将其从生产代码中删除。在这种情况下,没有理由将其从生产代码中删除。但在许多情况下,完整性测试可能会减慢速度。通过将这些测试放在生产代码中不使用的断言中,您可以自由地编写彻底的测试,而不必担心它们会减慢生产代码的速度。 (2认同)

Don*_*ows 12

断言用于检查后置条件和"永远不会失败"的前置条件.正确的代码永远不会失败一个断言; 当它们触发时,它们应该指出一个错误(希望在一个接近问题实际位置的地方).

断言的示例可以是检查以正确的顺序调用特定方法组(例如,hasNext()在之前调用next()的方法Iterator).

  • @DJClayworth:您也不需要避免触发断言.:-) (6认同)

Cir*_*四事件 7

Java中的assert关键字有什么作用?

我们来看看编译后的字节码.

我们将得出结论:

public class Assert {
    public static void main(String[] args) {
        assert System.currentTimeMillis() == 0L;
    }
}
Run Code Online (Sandbox Code Playgroud)

生成几乎完全相同的字节码:

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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

其中Assert.class.desiredAssertionStatus()true-ea传递在命令行上,否则为假.

我们用System.currentTimeMillis()它来确保它不会被优化掉(assert true;确实).

生成合成字段,以便Java只需要Assert.class.desiredAssertionStatus()在加载时调用一次,然后将结果缓存在那里.另请参阅:"静态合成"的含义是什么?

我们可以用以下方式验证:

javac Assert.java
javap -c -constants -private -verbose Assert.class
Run Code Online (Sandbox Code Playgroud)

使用Oracle JDK 1.8.0_45,生成了一个合成静态字段(另请参阅:"静态合成"的含义是什么?):

static final boolean $assertionsDisabled;
  descriptor: Z
  flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
Run Code Online (Sandbox Code Playgroud)

与静态初始化程序一起使用:

 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
Run Code Online (Sandbox Code Playgroud)

主要方法是:

 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
Run Code Online (Sandbox Code Playgroud)

我们得出结论:

  • 没有字节码级别支持assert:它是Java语言概念
  • assert可以使用系统属性很好地模拟在命令行上-Pcom.me.assert=true替换-ea,以及a throw new AssertionError().

  • 所以`catch(Throwable t)`子句能够捕获断言违规吗?对我来说,仅限于断言主体耗时的情况限制了它们的实用性,这种情况很少见. (2认同)
  • 我不确定为什么它会限制断言的用处。除非在极少数情况下,否则您不应该抓住 Throwable。如果您确实需要捕获 Throwable 但希望它不捕获断言,您可以先捕获 `AssertionError` 并重新抛出它。 (2认同)

sol*_*nmv 7

断言允许检测代码中的缺陷.您可以打开断言进行测试和调试,同时在程序投入生产时将其关闭.

当你知道它是真的时,为什么断言?只有当一切正常时,才会这样.如果程序存在缺陷,则可能实际上并非如此.在此过程的早期检测到这一点可以让您知道出了什么问题.

一个assert语句包含一个可选的沿着这条语句String的消息.

assert语句的语法有两种形式:

assert boolean_expression;
assert boolean_expression: error_message;
Run Code Online (Sandbox Code Playgroud)

以下是一些基本规则,用于管理应使用断言的位置以及不应使用断言的位置.断言应该用于:

  1. 验证私有方法的输入参数.不适用于公共方法.public传递错误参数时,方法应抛出常规异常.

  2. 在程序中的任何地方,以确保几乎可以肯定的事实的有效性.

例如,如果您确定它只是1或2,则可以使用如下的断言:

...
if (i == 1)    {
    ...
}
else if (i == 2)    {
    ...
} else {
    assert false : "cannot happen. i is " + i;
}
...
Run Code Online (Sandbox Code Playgroud)
  1. 在任何方法结束时验证发布条件.这意味着,在执行业务逻辑之后,您可以使用断言来确保变量或结果的内部状态与您期望的一致.例如,打开套接字或文件的方法可以在末尾使用断言来确保套接字或文件确实打开.

断言应该用于:

  1. 验证公共方法的输入参数.由于断言可能并不总是被执行,因此应该使用常规异常机制.

  2. 验证用户输入的内容的约束.与上述相同.

  3. 不应该用于副作用.

例如,这不是一个正确的用法,因为这里断言用于调用doSomething()方法的副作用.

public boolean doSomething() {
...    
}
public void someMethod() {       
assert doSomething(); 
}
Run Code Online (Sandbox Code Playgroud)

唯一可以证明这一点的情况是,当您试图找出代码中是否启用了断言时:   

boolean enabled = false;    
assert enabled = true;    
if (enabled) {
    System.out.println("Assertions are enabled");
} else {
    System.out.println("Assertions are disabled");
}
Run Code Online (Sandbox Code Playgroud)


Bjö*_*örn 6

一个真实世界的例子,来自Stack-class(来自Java文章中的Assertion)

public int pop() {
   // precondition
   assert !isEmpty() : "Stack is empty";
   return stack[--num];
}
Run Code Online (Sandbox Code Playgroud)

  • 这在C中是不受欢迎的:断言是绝对不会发生的事情 - 弹出一个空堆栈应该抛出一个NoElementsException或类似的东西.见唐纳德的回复. (80认同)
  • 那里可能存在内存泄漏.你应该设置stack [num] = null; 为了让GC正常工作. (7认同)
  • 我同意.尽管这是从官方教程中获取的,但这是一个糟糕的例子. (4认同)
  • 这应该是http://docs.oracle.com/javase/6/docs/api/java/ UTIL/LinkedList.html#pop()方法 (4认同)
  • 我认为在私有方法中,使用断言是正确的,因为对于类或方法的故障具有异常会很奇怪.在公共方法中,从外面的某个地方调用它,你无法真正告诉其他代码如何使用它.它真的检查isEmpty()与否?你不知道. (3认同)
  • @Konerak 如果合同禁止的话,弹出这个堆栈也应该“真的永远不会”发生。如果您希望捕获并处理异常,则异常需要具有不同的名称。如果应该在代码中修复诸如弹出此堆栈之类的错误,而不是在运行时处理,那么可以让它引发断言。 (2认同)

Iva*_*sov 5

除了这里提供的所有优秀答案之外,官方Java SE 7编程指南还有一个非常简洁的使用手册assert; 有几个现场例子说明何时使用断言是一个好的(而且重要的是,坏的)想法,以及它与抛出异常的方式有什么不同.

链接