稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Java中整数基础知识

最近做了一道题,非常有意思,题本身很简单,但涉及到整数的最大值以及最小值,当写测试用例的时候,却犯了一个错误,发现最小整数并不是0xFFFFFFFF,我们来仔细看一下。

整数基础

Java中,整数都是有符号的,最高位是符号位,0表示正数,1表示负数。有四种,byte,short,int和long。

  • byte 8位,-27 ~ 27 - 1,-128 ~ 127, 0x80 ~ 0x7F
  • short 16位,-215 ~ 215 - 1,-32768 ~ 32767, 0x8000 ~ 0x7FFF
  • int 32位,-231 ~ 231 -1,-2147483648 ~ 2147483647, 0x8000000 ~ 0x7FFFFFFF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
System.out.println(String.format("Max byte %d, 0x%X, half 0x%X", Byte.MAX_VALUE, Byte.MAX_VALUE, Byte.MAX_VALUE/2));
System.out.println(String.format("Min byte %d, 0x%X, half 0x%X", Byte.MIN_VALUE, Byte.MIN_VALUE, (byte)(Byte.MIN_VALUE/2)));
System.out.println(String.format("Max short %d, 0x%X, half 0x%X", Short.MAX_VALUE, Short.MAX_VALUE, Short.MAX_VALUE/2));
System.out.println(String.format("Min short %d, 0x%X, half 0x%X", Short.MIN_VALUE, Short.MIN_VALUE, (short) (Short.MIN_VALUE/2)));
System.out.println(String.format("Max Int %d, 0x%X, half 0x%X", Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE/2));
System.out.println(String.format("Min Int %d, 0x%X, half 0x%X", Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE/2));

// Outputs
//Max byte 127, 0x7F, half 0x3F
//Min byte -128, 0x80, half 0xC0
//Max short 32767, 0x7FFF, half 0x3FFF
//Min short -32768, 0x8000, half 0xC000
//Max Int 2147483647, 0x7FFFFFFF, half 0x3FFFFFFF
//Min Int -2147483648, 0x80000000, half 0xC0000000

细节和原理

值得注意的是,16进制的数值与直觉预期并不一样,特别是负数。正数是一致的,比如int,一共是32位,最高位是符号,所以真正数值部分是31位,那么最大的int就是0x7FFFFFF。

但负数,也即是最小的int,却与直觉完全不一样。按照直觉,负数最高位是1,那最小的int应该是0xFFFFFFFF啊,为何确是0x80000000呢?原因就是整数的编码方式并不是直接的二进制形式的,是以补码的形式,也就是说在内部实现中,用二进制表示一个整数的时候,是以二进制补码形式(转成二进制后还要求其补码,才是真实的二进制和16进制形式)。

简单来说,补码是一种二进制编码形式,正数的补码就是它的本身,而负数的补码是其取反后加1,可以参考百科上的定义

负数的转换:原码取反加1,即是补码,补码再转补码即得到原码(也可以补码减1再取反即是原码),符号位在转换过程中一直不变。

注意:补码的严谨说法是2的补码(Twi’s Complement),并不称作二进制补码。补码是为了能用加法的方式来计算减法。因此原码转补码时,取反加1,补码求原码时再求补码,避免用减法(虽然减1后再取反也能求得原码,但需要用到减法)。

非10进制字面常量是补码形式

在日常代码中,为了方便,通常都用16进制来写一些整数常量,这里就特别要注意了。16进制的字面常量,不会再进行补码转换,会当成补码直接使用。

1
2
System.out.println(String.format("Literal %d, 0x%X", 0xffffffff, 0xffffffff));
//Literal -1, 0xFFFFFFFF

所以,你写的0xFFFFFFFF是补码形式,它的原码是减1再取反,(32个1)减1,最低位变成0,前面31个1,再取反,就只剩下最后一位是1和最高位的符号位,因此是-1,注意符号位是不变的,在转换过程中。

而最小的整数是-231,原码 形式应该是0x80000000,先取反变成了0xFFFFFFFF,再加1,符号位最高位不变的情况下,其余全变成了0,所以是0x80000000。

最小整数

开篇时说了,当时错误的认为0xFFFFFFFF是最小的整数,这里犯的第一个严重错误是,误把二进制的补码当成了原码,代码中的16进制(二进制)都是补码形式的,它的原码是0x80000001即-1。这个错误是比较明显的。

但另外的问题就是,假如都是二进制原码的情况下,为啥最小的整数是0x80000000而不是0xFFFFFFFFF。这是理解上的误区,整数的定义是,最高位是符号位,所以常规认知是全是1的情况是最大的数,加上符号不就变成最小的了么?这是以10进制思维,也就是二进制转换成为10进制后的想法。计算机只认识二进制,在最高位是1(负数)的情况下,哪个数最小?当然0x80000000最小啊,它除了符号位全是0,肯定 小于0xFFFFFFFF,因此从二进制的角度来理解,0x80000000是最小的整数。

而0xFFFFFFFF(原码)则是第2小的负整数,最高位是符号位,其余31位全是1,它的补码是0x80000001:

1
2
System.out.println(String.format("Literal %d 0x%X", 0x80000001, 0x80000001));
//Literal -2147483647 0x80000001

计算机中是以二进制补码来存储整数的,所以要从计算机的角度来理解比较,就是要用二进制的补码来比较两个数的大小。

再次强调,我们写的源码当中的二进制(无论是字面常量,还是打印输出)都是补码形式,计算机看到的也是补码,比较也是补码,只有当转换成为10进制时,才会还原为原码并进行10进制转换

由此得出,(注意,程序员眼睛看到的16进制全是补码形式):

  • 0x80000000是最小的负数,原码为0x80000000,-231
  • 0x80000001,第2小的负数(最小的0x80000000再加上1),原码为0xFFFFFFFF,-(231-1)
  • 0xFFFFFFFF,是-1,原码为0x80000001。它是最大的负数(-1是最大的负数)。0xFFFFFFFF(全是1)肯定 最大啊,最高位是1,是负数,所以是最大的负数。

一些有意思的值

Integer.MAX_VALUE + 1 = Integer.MIN_VALUE

按理说应该溢出了,但如果以16进制去计算,就是这样的结果:0x7FFFFFFF + 1 = 0x80000000

1
2
System.out.println(String.format("Max in %d (0x%X) + 1 = %d (0x%X)", Integer.MAX_VALUE, Integer.MAX_VALUE, (Integer.MAX_VALUE+1), (Integer.MAX_VALUE+1)));
// Max in 2147483647 (0x7FFFFFFF) + 1 = -2147483648 (0x80000000)

Integer.MIN_VALUE - 1 = Integer.MAX_VALUE

1
2
System.out.println(String.format("Min in %d (0x%X) - 1 = %d (0x%X)", Integer.MIN_VALUE, Integer.MIN_VALUE, (Integer.MIN_VALUE-1), (Integer.MIN_VALUE-1)));
//Min in -2147483648 (0x80000000) - 1 = 2147483647 (0x7FFFFFFF)

参考资料

Comments