震惊!Integer的背后竟然是这个样子!!!

1,669 阅读7分钟

1. 疑问

最近在刷Leetcode的时候,遇到这样一个题:

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围[231-2^{31},2312^{31}]。请根据这个假设,如果反转后整数溢出那么就返回 0。

读完题后问了自己几个问题,发现自己都不清楚:

  1. 32位整数是多少?32位是指什么?
  2. 32为有符号整数的范围为什么是[231-2^{31},2312^{31}]?
  3. 二进制怎么表示?

掏出计算器用二进制算了一下,2322^{32}=4294967295,2312^{31}=2147483647

到这里,上面的问题基本上搞明白了,新的问题来了:

  1. 二进制怎么表示正负数
  2. 二进制的正负数怎么计算,计算机是怎么处理的
  3. 什么是原码?反码?补码?
  4. Java中是如何处理Integer的?

2. 开始入正题

2.1. 二进制、原码、反码、补码

为什么要有补码?

这里以4位bit为例,二进制最高位0代表+1代表-

整数原码
40 100
-41 100
00000
50 101
-51 101
20010
70 111
-71 111
50110

因为计算机中没有减法,我们发现,这些数的正负数之和,只有4的二进制计算是正确的,结果为0。为了解决这个问题,所以就有了反码、补码的概念。

整数原码反码补码
40 1000 1000 100
-41 1001 0111 100
0--0000
50 1010 1010 101
-51 1011 0101 011
0--0000
70 1110 1110 111
-71 1111 0001 001
0--0000

通过补码进行正负数的计算,结果就对了

2.1.1. 原码

最高位表示正负号,0代表整数,1代表负数,其它位表示数值的二进制

2.1.2. 反码

  • 正数的反码:与原码一致
  • 负数的反码:最高位符号位不变,其它位取反

2.1.3. 补码

  • 正数的补码:与原码一致
  • 负数的补码:反码 + 1

2.2. Java中的Integer

    @Test
    void testLocal() {
        Integer maxInt = Integer.MAX_VALUE;
        Integer minInt = Integer.MIN_VALUE;

        log.info("十进制:min = {}, max = {}", minInt, maxInt);
        String minBinary = Integer.toBinaryString(minInt);
        String maxBinary = Integer.toBinaryString(maxInt);
        log.info("二进制:minBinary {}位 = {}, maxBinary {}位 = {}", minBinary.length(), minBinary, maxBinary.length(), maxBinary);
        log.info("int 0 = binary {}", Integer.toBinaryString(0));

        log.info("{} = {}, {} = {}", minInt + 1, Integer.toBinaryString(minInt + 1), maxInt - 1, Integer.toBinaryString(maxInt - 1));
        log.info("越界intMax + 1 = {}", maxInt + 1);
    }

Java中的Integer的大小是32位bit,最小值和最大值范围就是[231-2^{31},2312^{31}],数值大小是31位,最高位0/1表示正负。

从二进制的角度来看,对正整数的最大值是31位1,加1后所有位进1变为0,最后结果就是最高位由0 -> 1,得到的二进制结果和负数的最小值相同,这样就容易理解java中的整数越界,以及Integer的范围。

2.3 int最小值补充解释(2021.3.2)

发现之前对java里Integer最大值最小值解释的不是很清楚,这里做一下补充。

此处有个注意点,负数的符号位用1来表示,那么int负数的最小值应该是32个1,为什么java中是1后面31个0呢。其实java里转为二进制后取的是负数的反码,我们看下转换过程

// 0 ~ 1 ~ 最大正整数2,147,483,647,一共2,147,483,648个数字,对应的二进制分别如下
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 = 0
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 = 1
0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 = 2,147,483,647

// 最小负整数-2,147,483,648 ~ -1,同样是2,147,483,648个数字
1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 = -2,147,483,648
1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 = -2,147,483,647
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 = -1

// Java中负数的二进制是有符号二进制的反码

// 1的二进制
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 = 1
// -1的转换过程
1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 = -1
// 负数的反码:最高位符号位不变,其它位取反
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1110
// 负数的补码:反码 + 1
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 = -1

通过这种计算方式,可以发现负数的二进制是补码。其中0比较特殊可以看做是[+0]和[-0],[+0]=0,[-0]=-2,147,483,648。这些都可以通过二进制的形式推导出来。

3. 总结

  1. 计算机中的计算只有加法和二进制,为了原码、反码、补码就是为了解决计算机中的运算
  2. 整数越界,就是在固定32位长度下,对二进制计算结果转为整数的过程
  3. 通过一个问题引申出一系列的问题,从数值范围 -> 二进制表示 -> 有/无符号 -> 二进制计算 -> 原码/反码/补码 -> 整数越界...

4. 最后

了解的二进制后,Java中的移位运算符("<<"、">>"、">>>")是怎么做运算的?