「译文」为什么 2147483648 如此特别
在做 LeetCode 7. 整数反转 时发现 2147483648 这个神奇的数字,我使用的是 Java,判断 -(-2147483648)==(-2147483648) 时,得到结果是 true,因此决定一探究竟(文章最后简单说明了为何这个判断为 true)。
浏览资料时发现了这篇文章 What’s so special about 2147483648?,觉得有趣,于是决定翻译下来。
为什么 2147483648 如此特别
首先,它是 2 的幂。无疑,这不是一件明显的事情,除非你是某种数学天才。而且精确的说,它是 2^31。这很重要——稍后你就会看到。
在二进制里面,2147483647 是 01111111111111111111111111111111,当使用补码表示法(”two’s complement” notation)时,它是可容纳在 32 位比特中最大的正整数——这种表示数字的方法允许表示负数。如果我们能够使用最左边(高位)的比特,最大的数字将会变成两倍大,因为在一个二进制数字中每个额外的比特能够表示双倍的范围。
你会在运算 256 256 256 * 256 然后除以 2 时得到 2147483648,还没感到激动吗?再给我一分钟的时间。
在 4 字节或 32 位比特数字中,第 32 位比特的数字被用来代表这个数字是正数还是负数。如果这一位是 0,譬如 01111111111111111111111111111111,这个数字就是 +2147483647;如果是 1,譬如 11111111111111111111111111111111,这个数字就是 -2147483648。
因此,Unix 系统中有一种表示更大数字范围的方法,也有表示负数的方法。但这并不意味你不能使用更大的数字,在现代 Linux 系统中,2147483648 甚至远没接近你能操作的最大的数字。看下在我 AWS 系统的终端中一个例子的部分代码
1 | $ echo $n |
很明显地,你能够在终端中操作的数字在一个大得多的范围中,即便如此,2147483647 仍享有一定的声誉。这个数字代表着在 Unix 系统上能够表示的最近(即最远的未来)的日期——至少在现在来说。2038 年前的某个时候,Unix 的各种 “风味” 的开发者(译者:这里不是很懂该如何翻译)必须想清楚下一步该怎么做。为何这件事如此重要,以及为何是 2038 年?
要理解这件事为什么如此重要,你必须考虑 Unix 是如何存储日期的。要在二进制中看当前的日期及时间,要使用类似这样的日期命令:
1 | date +%s |
你可能已经听说过 Unix “纪元” 的引用。或多或少,它暗示着 Unix 的诞生日期,这个时间指向 1970 年 1 月 1 日。如果你在那天创建了一份文件,这份文件的创建日期会在系统中存储为 0。如果我们使用日期命令的 -d
选项,我们可以类似如下这样,将日期 “0” 翻译成具体的日期/时间字符串:
1 | $ date -d @0 |
并且,若我们使用日期的最大值来替代 0,我们会得到:
1 | $ date -d @2147483647 |
反过来,最小日期时间会从纪元时间(1970-01-01)向前跳转跟最大日期向后跳转相同的秒数:
1 | $ date -d@-2147483648 |
这意味着在 Unix 系统中能被存储的最大日期是 2038 年 1 月 19 日,以及,像 16 年前让人们担心的 Y2K 问题一样,当系统和设备的时间从 01111111111111111111111111111111 跳到 10000000000000000000000000000000 时,很可能被解析为 1901 年 1 月 19 日,因此 2038 问题正威胁扰乱系统及设备。等到那时,解决方法可能是增加另一个或两个字节来表示日期/时间,也有可能停止使用补码格式的时间表示方式,来让我们再有 68 年的时间来担心这个问题。希望我们不会有太多无法适配新日期格式的恒星或行星系统。
Y2K 确实是一个问题,因为众多应用将年份存储为 2 位数字段格式,一旦 2000 年开始,看上去将会与 1900 年相似。然而,尽管有暴风雨般的问题,我们进入 21 世纪的时候,许多人期望的问题却更少了。
但 2038 问题要花费更多的时间和关注,因为它涉及到日期的秒精度的内部表示方法。而且这些日期和时间危害着许多流程和设备。不过现在只是 1449012345,我们还有时间。
后记
现在再来回答为什么 Java 中 -(-2147483648)==(-2147483648)。
其实不单是 Java,任何 32 位字节存储整数的系统都存在这个问题,因为在计算机内部,整数是以二进制形式存储的,而要想让二进制数值表达「负数」的概念,就需要一位标志位。计算机中采用了上文所述的最高位标志位。
Java int 类型整数最多占用 32 位字节空间,约定最小的负数 -2147483648,二进制表示为 1000…(符号位 1 后跟 31 个 0),且其相反数同样为 1000…(符号位 1 后跟 31 个 0),你可以试试下面的代码
1 | int testValue = -2147483648; |
这是因为,1000…(符号位 1 后跟 31 个 0)可以用来表示 -0,但这与 0 的二进制表示法 0000(共 32 个 0)不同,造成了 0 有两种表示方法,使得二进制与十进制的互换不再是一一对应的关系。因此约定了其中的一种方法表示为 -2147483648,所以负数的最小值绝对值比整数的最大值绝对值多 1。
你可以看这篇文章了解更加详细的推理过程:int类型在内存中的存储方式
END