最近做了一道题,非常有意思,题本身很简单,但涉及到整数的最大值以及最小值,当写测试用例的时候,却犯了一个错误,发现最小整数并不是0xFFFFFFFF,我们来仔细看一下。
Java中,整数都是有符号的,最高位是符号位,0表示正数,1表示负数。有四种,byte,short,int和long。
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后再取反也能求得原码,但需要用到减法)。
在日常代码中,为了方便,通常都用16进制来写一些整数常量,这里就特别要注意了。16进制的字面常量,不会再进行补码转换,会当成补码直接使用。
System.out.println(String.format("Literal %d, 0x%X", 0xffffffff, 0xffffffff));
//Literal -1, 0xFFFFFFFF
所以,你写的0xFFFFFFFF是补码形式,它的原码是减1再取反,(32个1)减1,最低位变成0,前面31个1,再取反,就只剩下最后一位是1和最高位的符号位,因此是-1,注意符号位是不变的,在转换过程中。
而最小的整数是-2^31,原码 形式应该是0x80000000,先取反变成了0xFFFFFFFF,再加1,符号位最高位不变的情况下,其余全变成了0,所以是0x80000000。
开篇时说了,当时错误的认为0xFFFFFFFF是最小的整数,这里犯的第一个严重错误是,误把二进制的补码当成了原码,代码中的16进制(二进制)都是补码形式的,它的原码是0x80000001即-1。这个错误是比较明显的。
但另外的问题就是,假如都是二进制原码的情况下,为啥最小的整数是0x80000000而不是0xFFFFFFFFF。这是理解上的误区,整数的定义是,最高位是符号位,所以常规认知是全是1的情况是最大的数,加上符号不就变成最小的了么?这是以10进制思维,也就是二进制转换成为10进制后的想法。计算机只认识二进制,在最高位是1(负数)的情况下,哪个数最小?当然0x80000000最小啊,它除了符号位全是0,肯定 小于0xFFFFFFFF,因此从二进制的角度来理解,0x80000000是最小的整数。
而0xFFFFFFFF(原码)则是第2小的负整数,最高位是符号位,其余31位全是1,它的补码是0x80000001:
System.out.println(String.format("Literal %d 0x%X", 0x80000001, 0x80000001));
//Literal -2147483647 0x80000001
计算机中是以二进制补码来存储整数的,所以要从计算机的角度来理解比较,就是要用二进制的补码来比较两个数的大小。
再次强调,我们写的源码当中的二进制(无论是字面常量,还是打印输出)都是补码形式,计算机看到的也是补码,比较也是补码,只有当转换成为10进制时,才会还原为原码并进行10进制转换。
由此得出,(注意,程序员眼睛看到的16进制全是补码形式):
按理说应该溢出了,但如果以16进制去计算,就是这样的结果:0x7FFFFFFF + 1 = 0x80000000
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)
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)