好吧,在我有限的 Java 经验中,我从来没有真正使用过断言,我想知道为什么我在很多网站上阅读了很多关于断言的书籍,同样的警告是断言语句不应该用于参数检查在公共方法中?
我想知道这是否与断言语句相对于 Java 中其他语句的执行顺序有关。
非正式地,参数检查和断言服务于不同的目的:
本质上,当你断言一个条件时
assert val < total;
该检查向您的代码的读者传达了以下简单的英语思想:“我检查了我的代码,根据我的推理,我确信它val
总是小于total
”。
val
另一方面,当您检查参数时,
if (val >= total) throw new InvalidArgumentException("val");
您的代码说“调用者忘记确保val
小于total
”。
这是两种不同的想法,因此很自然地采用两种不同的方式在代码中传达它们。
断言的目的是检查你的程序逻辑——断言失败是“停止一切——有一个错误!” 指示。特别是,一个断言失败表明“这里有一个错误”,但“这里”是你代码内部的某个地方,失败的原因只能通过检查你的代码来确定(你的 API 的用户不能而且应该预计不会这样做)。
当您通过 API 获得不良数据时,您想指出“嘿!您给了我不良数据!” IllegalArgumentException 及其亲属是表明这一点的方式。
(但请注意,在代码中对参数使用断言检查并没有错——在这种情况下,您不支持将由团队以外的人使用的真正“公共”API。)
但这确实带来了另一点:在合理/可能的范围内,您应该“捕获”由于您自己的错误而可能发生的 IllegalArgumentException 之类的内部异常,并将它们转换为 FatalError 异常或类似的异常,因此您的 API 的用户当您的代码中存在错误时,不会导致他去寻找错误的参数。
(还要注意这里的区别public
——Java 关键字——和“公共接口”——意味着一些接口作为“正式”API 提供给你的编程团队之外的个人使用。后一种情况是我们'在这里担心。)
根据断言编程
参数检查通常是方法的已发布规范(或合同)的一部分,无论断言是启用还是禁用,都必须遵守这些规范。使用断言进行参数检查的另一个问题是错误的参数会导致适当的运行时异常(例如 IllegalArgumentException、IndexOutOfBoundsException 或 NullPointerException)。断言失败不会引发适当的异常。
Mike 给出了一个很好的答案,不幸的是,这在 Java 文献中很少得到辩护。对 Mike 观点的一些额外支持:
绝大多数 Java 文献都传播了您不应该assert
用来检查公共方法参数的教条。换句话说,他们说不assert
应该用它来检查
公共方法的先决条件,而应该使用显式if (!precond) throw SomeException();
指令。标准 Java 库中充满了这种策略的示例。
支持这一点的论据似乎是:
嗯,这对我来说似乎是一种非常傲慢的态度。您的代码的客户是程序员,就像您一样。满足前提条件当然是客户的责任,客户不做就是bug;客户的错误,而不是你的。当然,您希望您的客户使用启用的断言检查他们的程序,不是吗?
但是一旦他们确信他们的程序是正确的,你为什么还要把无用的异常强加给他们呢?
现在,从客户的角度来看它。你正在使用String.charAt
. 此方法的文档告诉您
public char charAt(int index)
返回指定索引处的 char 值。索引范围从 0 到 length() - 1。 [...]
前提条件明确。
但后来它增加了
抛出: IndexOutOfBoundsException - 如果 index 参数为负数或不小于此字符串的长度。
所以,如果你确定你的索引在界限内,你还会把你的电话放在 a 内try ... catch
吗?请注意,文档并没有真正告诉您,如果您遵守先决条件,则不会引发异常,但是这当然是您所期望的,不是吗?
因此,您确定索引在范围内,您甚至可能以前曾断言过,并且您不会浪费时间和空间与无用try
的永远不会捕获任何东西,但String
仍然会浪费时间检查您是表现良好。
在我看来,
这是我教给我的学生的。我不告诉他们。这不是教条。我让他们知道这与他们在大多数书中读到的相反,但我敦促他们自己决定。
这个问题不是 Java 特有的;这是一个关于编程方法的问题。
我遵循的方法类似于按合同设计。一些语言具有支持这种风格的特殊语法,例如显式声明前置条件、后置条件和对象不变量的可能性,并且这些明确包含在代码的公共规范(和文档)中。
一方面,Java 断言在运行时被删除,除非它们在编译时显式启用。
异常更适合参数验证,因为您希望处理它们,而断言具有语义含义“这在代码中的这一点必须是正确的,否则我不知道如何处理它”。
断言不应该用于公共方法中的参数检查的概念是完全错误的。仅仅因为你在书中读到了什么并不意味着它是正确的。
公共方法中的参数检查完全属于检查错误的一般类别,因此应该这样对待,我们已经拥有的捕捉错误的机制是断言。
如果一个方法的公共接口说“这个方法的'index'参数永远不应该是负数”,那么用负索引调用它是一个错误,以下情况成立:
它不应该发生在生产环境中。(测试应该保证这一点。)
即使它发生在生产环境中,任何人都无法采取任何措施来缓解这个问题,因此它可能会因索引越界或空指针异常而进一步失败,这没有什么区别。
没有人应该依赖那里的支票。(故意允许使用无效参数调用该方法,捕获产生的 IllegalArgumentException,并尝试采取纠正措施是一个坏主意。)
事实上,保证即使在生产中对于您认为是错误的条件也会抛出特定异常会诱使 n00b 程序员编写依赖于在生产中抛出的异常的代码,因此,本身就是一个错误。
错误的概念也是断言表明“此处”存在错误。如果断言检查方法参数,则断言捕获的错误可能位于堆栈跟踪中列出的方法调用链中的任何位置,甚至可能位于其他位置。
那里有数十亿台设备,其中大多数都依靠宝贵的电池电量运行,每天执行数万亿条指令,这些设备只是在经过数百万工时的艰苦测试保证永远不会发生的条件下进行测试由开发人员和测试人员推出。这太疯狂了。
assert
所以,对于参数检查来说,只是简单的就可以了。如果您需要编写测试代码以确保您的方法确实针对特定的错误条件正确断言,请考虑以下构造:
assert index >= 0 : new IllegalArgumentException( "index" );
显然,这只会在启用断言的情况下执行测试,但它真正的美妙之处在于,如果断言失败,那么cause
异常的AssertionError
将是IllegalArgumentException
,因此您的测试代码将能够确保捕获到正确的错误.
有关此主题的更多信息,请参阅我博客上的这篇文章:michael.gr - 断言和测试
因为断言在生产版本中被禁用。如果您需要能够在公共方法使用不正确时进行捕获,那么断言不会触发生产构建中的检查,而异常将是发出错误信号的更好方法。
这对于库来说尤其重要,因为您无法控制调用谁以及如何调用您的方法;对于应用程序,只要有正确的输入验证(其中“输入”可能是用户输入,或者来自另一个系统或来自持久存储的输入),公共方法中的断言就可以了,那么断言永远不应该被触发。
使用 assert 来检查不应该发生的问题,在公共 API 中也是如此。
将其视为可编译(并运行,如果您愿意)的文档。