if (a - b < 0)和if (a < b)的区别是什么?(涉及IT技术)

254

我正在阅读Java的ArrayList源代码,并注意到一些if语句中的比较。

在Java 7中,方法grow(int)使用

if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

在Java 6中,grow方法并不存在。但是ensureCapacity(int)方法会使用

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

改变的原因是什么?是性能问题还是样式问题?

我可以想象与零比较会更快,但仅为了检查是否为负数执行完整减法似乎有点过头了。此外,就字节码而言,这将涉及两个指令(ISUBIF_ICMPGE),而不是一个指令(IFGE)。


35
在防止溢出方面,if (newCapacity - minCapacity < 0)if (newCapacity < minCapacity)更好是因为它避免了在两个大整数相加时可能发生的溢出错误。 - Eran
3
我不确定提到的标志溢出是否真的是原因。减法似乎更容易发生溢出。该组件可能在说“这仍然不会溢出”,也许两个变量都是非负的。 - Joop Eggen
12
FYI,你认为进行比较比执行“完整减法”更快。根据我的经验,在机器代码级别上,通常是通过执行减法、丢弃结果并检查所得标志来完成比较的。 - David Dubois
6
@David Dubois:OP并没有假设比较操作比减法操作更快,而是认为与零比较可能比两个任意值的比较更快,并且正确地认识到在进行实际减法操作以获得要与零比较的值时,这种情况不成立。这很合理。 - Holger
4个回答

286

a < ba - b < 0可能有两种不同的含义。请看下面的代码:

int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
    System.out.println("a < b");
}
if (a - b < 0) {
    System.out.println("a - b < 0");
}

运行时,这段代码将只打印a - b < 0。发生的情况是,a < b 明显为假,但 a - b 溢出并变成了负数 -1

现在,考虑数组长度非常接近于 Integer.MAX_VALUE。在 ArrayList 中的代码如下:

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

oldCapacity非常接近Integer.MAX_VALUE,因此newCapacity(即oldCapacity + 0.5 * oldCapacity)可能会溢出并变为Integer.MIN_VALUE(即负数)。然后,减去minCapacity将向下溢出为正数。

这个检查确保if不会被执行。如果代码编写为if (newCapacity < minCapacity),在这种情况下它将是true(因为newCapacity为负),因此newCapacity会被强制设置为minCapacity,而不考虑oldCapacity

这种溢出情况由下一个if处理。当newCapacity溢出时,这将是trueMAX_ARRAY_SIZE定义为Integer.MAX_VALUE-8,且Integer.MIN_VALUE - (Integer.MAX_VALUE-8)> 0 true。因此,newCapacity得到了正确的处理:hugeCapacity方法返回MAX_ARRAY_SIZEInteger.MAX_VALUE

NB:这就是该方法中// overflow-conscious code注释所表示的意思。


8
数学和计算机科学的区别好的演示。 - piggybox
36
@piggybox,我不这么认为。这是数学。只不过它不是在Z上的数学,而是在模2^32的整数版本上进行(并且所选择的规范表示与通常不同)。这是一个合适的数学系统,不仅仅是“哈哈,计算机和它们的怪癖”。 - harold
2
我本来会写出完全不会溢出的代码。 - Aleksandr Dubinsky
如果我没记错,处理器通过执行 a - b 并检查最高位是否为 1 来实现有符号整数的小于指令。它们如何处理溢出? - Ky -
2
@BenC.R.Leggiero x86等处理器通过状态标志位在单独的寄存器中跟踪各种条件,以便与条件指令一起使用。该寄存器具有用于结果符号、结果是否为零以及上溢/下溢是否发生在最后一个算术操作中的单独位。 - user824425

105

我找到了这个解释:

在2010年3月9日,Kevin L. Stern写道:

我进行了一个快速搜索,发现Java确实基于二进制补码。尽管如此,请允许我指出,总的来说,这种类型的代码让我感到担忧,因为我完全期望在某个时候会有人来做Dmytro建议的事情;也就是说,有人会改变:

if (a - b > 0)

if (a > b)

整艘船都会沉没。个人而言,我喜欢避免使用晦涩难懂的算法,除非有很好的理由使用整数溢出作为算法的基础。通常情况下,我更喜欢完全避免溢出,并使溢出场景更加明确:

if (oldCapacity > RESIZE_OVERFLOW_THRESHOLD) {
   // Do something
} else {
  // Do something else
}

这是一个很好的观点。

ArrayList中,我们不能这样做(或者至少不兼容),因为ensureCapacity是公共API并且已经将负数解释为无法满足的正容量请求。

当前API的使用方法如下:

int newcount = count + len;
ensureCapacity(newcount);

如果你想避免溢出,你需要改变成一些不那么自然的东西,比如

ensureCapacity(count, len);
int newcount = count + len;
无论如何,我会保留对溢出的注意代码,但是增加更多的警告注释,并对大型数组创建进行"轮廓",以便ArrayList的代码现在看起来像:
/**
 * Increases the capacity of this <tt>ArrayList</tt> instance, if
 * necessary, to ensure that it can hold at least the number of elements
 * specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
public void ensureCapacity(int minCapacity) {
    modCount++;

    // Overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // Overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

Webrev 已经重新生成。

Martin

在 Java 6 中,如果您使用 API 如下:

int newcount = count + len;
ensureCapacity(newcount);

如果 newCount 溢出(变为负数),if (minCapacity > oldCapacity) 将返回 false,您可能会错误地认为 ArrayList 增加了 len


2
好主意,但它被ensureCapacity的实现所否定。如果minCapacity是负数,你永远不会到达那个点——它就像复杂的实现一样被默默地忽略了。因此,“我们不能这样做”以保持公共API兼容性是一个奇怪的论点,因为他们已经这样做了。唯一依赖于这种行为的调用方是内部调用方。 - Holger
1
@Holger 如果 minCapacity 很负(即从将当前 ArrayList 大小加上您希望添加的元素数时发生了 int 溢出),则 minCapacity - elementData.length 会再次溢出并变为正。这是我理解的方式。 - Eran
1
@Holger,然而在Java 8中他们再次更改为if (minCapacity > minExpand),这一点我不太理解。 - Eran
是的,这两个addAll方法是唯一一个可能会导致当前大小和新元素数量之和溢出的情况。然而,这些都是内部调用,而且“我们不能更改它,因为ensureCapacity是公共API”这个论点是一个奇怪的论点,事实上,ensureCapacity确实忽略负值。Java 8 API没有改变这种行为,它所做的只是在ArrayList处于初始状态(即使用默认容量初始化并仍为空)时忽略低于默认容量的容量。 - Holger
换句话说,当涉及内部使用时,“newcount = count + len” 的推理是正确的,但它不适用于“public”方法“ensureCapacity()”… - Holger

19

看代码:

int newCapacity = oldCapacity + (oldCapacity >> 1);
如果oldCapacity非常大,它将会溢出,导致newCapacity成为一个负数。像newCapacity < oldCapacity这样的比较将错误地评估为true,并且ArrayList将无法增长。
相反,按照代码编写方式(newCapacity - minCapacity < 0返回false),将允许对newCapacity的负值在下一行进一步评估,从而通过调用hugeCapacitynewCapacity = hugeCapacity(minCapacity);)重新计算newCapacity来允许ArrayList增长到MAX_ARRAY_SIZE
这就是// overflow-conscious code注释试图传达的内容,尽管有些含蓄。
因此,归根结底,新的比较保护了不会分配超过预定义的MAX_ARRAY_SIZEArrayList,同时允许其在需要时增长到该限制。

2
这两种形式的行为完全相同,除非表达式 a - b 溢出,在这种情况下它们是相反的。如果 a 是一个很大的负数,而 b 是一个很大的正数,则 (a < b) 显然为真,但 a - b 将溢出变为正数,因此 (a - b < 0) 为假。
如果您熟悉 x86 汇编代码,请考虑 (a < b) 是通过 jge 实现的,当 SF = OF 时跳过 if 语句的主体。另一方面,(a - b < 0) 的行为类似于 jns,当 SF = 0 时跳转。因此,只有当 OF = 1 时,它们的行为才会有所不同。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接