为什么 Java 整型的负数比正数多一个数字范围?

542 阅读7分钟

Java 的整型有 byte、short、int、long 四种,其对应的数值范围如下所示:

整型可取最小值可取最大值
byte-128127
short-3276832767
int-21474836482147483647
long-92233720368547748089223372036854774807

        所有的负数范围都比整数多 1 个数字,其实这是计算机的存储和加减运算机制决定的。

        首先,计算的存储只有 0 和 1,每个位置要么存 0,要么存 1,这些位置又叫做位(即 bit)。

        其次,拿 byte 举例,它在目前计算的标准中是 8 位的,也就是说:1 byte = 8 bit;所以一个 byte 在计算机中只有 8 个可以存放 0 和 1 的位置,8 个位置放上 0 或 1,穷举的全部可能性为 2 的 8 次方,即 2^8=256。所以,在一个 byte 中最多只能表示 256 个不同意义的事物(可以是任何可能的事物),在这里如果是数字的话,就只能有 256 个数字了。如果我们不需要用它表示负数,那么它可以表示 0至255 这 256 个数字;如果它需要标识正负,计算机中会用高位表示符号,其他位表示数字,而 0 表示正号,1 表示负号。这时候 0 开头的二进制数字表示的范围是 0至127,而 1 开头的二进制数字表示的范围是 -0至-127,所以被正负号占用一位后,实质只能表示 -127至127 这 255 个数字而不是 256 个数字。

        但是,0 和 -0 本质上是没什么区别的,如果能够将 -0 改为代表其他数字,那么表示的范围就能增加,所以就有了:-0 表示 -1、-1 表示 -2、以此类推到 -127 表示 -128 这样的情况。

        虽然上述可以增加范围,但实际中不会有人为了增加一个数字范围而改变这种人类不容易理解的表达方式的,之所以实际情况确实如此表示,是有深层原因的:计算机人员为了简化计算机的计算过程和提高效率,不想让计算机先判断数字的正负,再使用加法或减法来计算,而是简化成:计算机只需要进行加法操作,并且忽略正负号的识别。

        要达到这个目的的理论很简单,就是 1-2 可以表示为 1+(-2) 的,还是用 byte 举例,

第一步:1-2=-1    这种适合人类思维的计算过程是:00,000,001 - 00,000,010 = 10,000,001(十进制为 - 1)

        这里是识别正负号再人工计算的,这对于计算机来说不够简单,效率也不高;

第二步:1-2 改为:1+(-2)=-1    后,其计算过程为:00,000,001 + 10,000,010 = 10,000,011(十进制为 - 3)

        显然这个计算过程是不正确的,为了能够直接做加法,还需要对负数取反后计算,即将负数除了符号位,其他位全部取反,10,000,010 取反后就是 11,111,101—— 这种取反后的二进制码也叫反码,取反前的二进制码则叫做原码。(顺便说明下反码的转换过程:正数的反码是其本身,负数的反码是除了符号位外其他位全部取反;反码转正码只需要逆转此过程)

第三步:将原码的计算过程取反后的计算过程为:

        (原)00,000,001 + (原)10,000,010 = (原)10,000,011(十进制为 - 3)

        (反)00,000,001 + (反)11,111,101 = (反)11,111,110(转为正码为 10,000,001,十进制为 - 1),计算过程正确。

        可见反码的出现完全是为了简化计算机的底层计算而存在的,这种计算方式忽略了符号仅使用加法就解决了正负数的问题。

        但是,反码还存在某些问题,如果上面的 1-2 是 2-2 的话,那么结果就是下面的:

        (原)00,000,010 + (原)10,000,010 = (原)10,000,100(十进制为 - 4)

        (反)00,000,010 + (反)11,111,101 = (反)11,111,111(转为正码为 1,000,000,十进制为 - 0),终于说到这个 - 0 了,这个从人的角度来看当然就是 0 的意思,但是对于计算机来说,还需要编指令告诉它:-0 就是 0,不然在计算机眼里它是两个不同的数字或信息。这样一来,就又是增加了计算机的复杂度,所以为了方便和效率,人们想出让 - 0 表示 - 1,-1 表示 - 2…… 这样的方式,结果就使得负数会比正数多一个数字的情况了 —— 所以使用这种表达方式的原因并不是为了多增加一个数字范围,而是降低计算机的计算复杂度,毕竟前者相比后者,根本不值一提。

        但是使用了这种表示方式后,计算机要怎么计算呢?这时候就需要用到补码了。(知识点来了,补码的转换过程:正数的补码是其本身,负数的补码是除了符号位外其他位全部取反后加 1,也就是反码再加 1;补码转正码或反码只需要逆转此过程)

第四步:

        这时候我们要讲清楚的是 - 0 的问题,所以我们拿 2-2 的例子来说明,将原码转为补码后的计算过程为:

        (原)00,000,010 + (原)10,000,010 = (原)10,000,100(十进制为 - 4)

        (反)00,000,010 + (反)11,111,101 = (反)11,111,111(转为正码为 1,000,000,十进制为 - 0)

        (补)00,000,010 + (补)11,111,110 = (补)100,000,000(转为正码前需要舍去高位 1,因为我们本来就是只有 8 位的,现在变成 9 位了,是存不下的,结果为 00,000,000,十进制为 0)

        这时候就再也没有 - 0 出现了,而舍位操作并没有对计算过程造成任何影响,并且还将 - 0 转成了 0(100,000,000 是 - 0,00,000,000 则是 0),真的是不得不敬佩想出这整套方法的科学家。

小结:这里主要讲述了计算机的计算过程,而这种计算过程的设计方式引出了反码和补码的概念 —— 所以本文还应该有个副标题:为什么计算机要有反码和补码,明白这些原理,便能轻松地掌握这些基础知识,而不是依靠强行记忆的填鸭式的学习过程。

        因为计算机最终是保存补码进行计算的,这样表示的负数就总会比正数多一个数字,而 Java 也是直接使用计算机的这套规则的, 自然结果都是一样的。

        而要是简单回答这个问题,那么可以简单归纳为:因为原码、反码、补码的规则导致了 Java 整型的负数比正数多一个数字范围。