带负数的里程表 —— a < b 还是 a - b < 0?
现在我们的int
里程表是这样的:
- 范围是
-5000
到4999
(模拟int
从负数到正数)。 - 超过最大值
4999
会怎么样? 再走1公里,它会溢出,变成-5000
。 - 低于最小值
-5000
会怎么样? 再倒车1公里,它会下溢,变成4999
。
场景:
你是个超级司机,已经开了 4990
公里 (newCapacity = 4990
),这已经快爆表了。
老板要求你至少开 4995
公里 (minCapacity = 4995
)。
你接着开了10公里去完成任务。
你的里程表从 4990
开始增加:4991
-> 4992
-> 4993
-> 4994
-> 4995
-> 4996
-> 4997
-> 4998
-> 4999
-> -5000
最终,你的里程表显示:-5000
现在我们来判断任务是否完成。
方法一:直接比较(错误方法)
- 你的里程表(newCapacity):
-5000
- 老板的要求(minCapacity):
4995
你用直接比较法:-5000 < 4995
?
是的,-5000
显然小于 4995
。
结论:里程不够!
但这完全错了!你明明已经开了整整5000公里(从4990到-5000),远远超过了老板要求的4995公里!只是里程表溢出成了负数。这个方法在极端情况下失效了。
方法二:减法比较(Java源码的方法)
你的里程表显示:-5000
老板的要求:4995
Java源码的方法:“请你用你的里程,减去老板的要求,看看结果是正还是负?”
计算:-5000 - 4995
这个计算本身也会溢出! 我们来算一下:
-5000 - 4995
数学上等于-9995
。- 但我们的里程表只能显示
-5000
到4999
,-9995
远远小于最小值-5000
。 所以结果会下溢,绕一大圈变成正数。
-5000 - 1 = -5001
(低于-5000,下溢) ->4999
-5000 - 2 = -5002
->4998
- ...
-5000 - 4995 = -9995
会下溢成-9995 - (-5000) + 4999 + 1
的复杂计算,最终结果是一个小的正数,比如5
。
最终,(-5000 - 4995)
在这个里程表世界里,结果是一个正数,比如 5
。
现在,Java源码的条件是:(你的里程 - 老板要求) < 0
我们算出来的结果是 5
(一个正数)。5 < 0
吗?不。
结论:里程足够!
这个结论是正确的!
为什么“溢出后的减法”能得出正确结论?
核心奥秘在于:CPU进行加减法时,并不关心数字的“数学值”,它只关心比特位(0和1)的运算。
- 符号位是裁判:CPU做完减法后,会看一下结果的最高位(符号位)。如果是
0
,就是正数或零;如果是1
,就是负数。 - 溢出不影响裁判:即使加减过程中发生了溢出,导致比特位全部错乱,这个“符号位”的判断规则也永远不会错。它依然能忠实地告诉你,在计算机的世界里,这次减法的结果是正还是负。
逻辑关系保持不变:最关键的是,
a - b
的运算结果的符号位,和a < b
这个逻辑关系的真假,在CPU的运算规则下是完全一致的。即使发生溢出,这个对应关系也雷打不动。- 如果
a < b
,那么a - b
在CPU看来结果就是负数(符号位为1)。 - 如果
a >= b
,那么a - b
在CPU看来结果就是正数或零(符号位为0)。
- 如果
所以,if (a - b < 0)
这个条件,本质上是在问CPU:“刚才的减法,结果符号位是1吗?”
而直接比较 if (a < b)
,在溢出时,比较的双方 a
和 b
本身就已经是错误的值了,所以会得出错误结论。
Java源码相信的是CPU硬件级别的、永不出错的符号位判断,而不是已经溢出失真的数值本身。这就是这个技巧如此强大和可靠的原因。
本文系作者 @xiin 原创发布在To Future$站点。未经许可,禁止转载。
暂无评论数据