字节的原码、补码与反码

近期项目中经常需要处理串口数据的解析,在将收到的字节进行校验、计算、转换时遇到了一些问题。其实只要了解Java对基本数据类型的处理方式,这些问题就可以很好的理解并解决。

从串口中收到一个长度为2的字节数组,现在要以大端模式读取这2个字节表示的正数数值:

byte[] array = readSerialBytes();
int result = (array[0] << 8) + array[1];

array[0]左移8位再加上array[1],这样计算看起来没什么问题,但总会遇到一些数据的计算值和实际值不同,比如:

array[0] = 0x81 = 1000 0001(二进制) = 129(十进制)
array[1] = 0x81 = 1000 0001(二进制) = 129(十进制)

理论上计算结果应该是0x818133153,但按上面的计算方法结果却是-32639。很奇怪吧,要找出导致这种情况的原因,首先需要理解关于字节的几个概念。

机器数与真值

任何数据,都是以二进制位的形式进行计算和存储的。在计算机中,以最高位作为符号位,1表示负数,0表示正数,其余为数据位,这种方式存储的数据称为机器数。至于真值,就是机器数所表示的真实数值,所以真值是有符号的。

以1个字节为例,最高位为符号位,有7个数据位,所能表示的真值范围是-(2^7-1)~+(2^7-1)-127~+127

反码与补码

反码补码都指的是机器数,其中:

  • 正数反码是其本身,负数反码是除符号位外,其余位取反。
  • 正数补码是其本身,负数补码是除符号为外,其余位取反,然后加1,即负数反码加1。
机器数 真值 反码 补码
0000 0001 1 0000 0001 0000 0001
1000 0001 -1 1111 1110 1111 1111

那么为什么一定要有反码补码呢,这其实和计算机本身的计算特性有关。计算机的二进制运算电路用与非门设计加法器很简单,而减法器的电路设计却非常复杂。为了节约计算时间和电路成本,用加法器实现减法就是一个比单独制造减法器更高效的方案,而反码补码正是为解决这个运算转换而产生的。

对于1-1 = 1+(-1)这样的计算,如果用原码

1-1 = 1+(-1) = 0000 0001(原码) + 1000 0001(原码) = 1000 0010(原码) = -2(真值)

显然是不正确的,如果对负数使用反码

1-1 = 1+(-1) = 0000 0001(反码) + 1111 1110(反码) = 1111 1111(反码) = 1000 0000(原码) = -0(真值)

虽然正0和负0都是0,但一般认为0是不应该有符号的,用补码就可以消除这个潜在的歧义:

1-1 = 1+(-1) = 0000 0001(补码) + 1111 1111(补码) = 0000 0000(补码) = 0000 0000(原码) = 0(真值)

结果是正确的0,这也就是反码补码存在的意义。

而且,正因为补码可以极大简化加减运算,方便用程序实现乘除运算,所以计算机对所有正数负数都是用补码表示的。

public static void main(String[] args) throws InterruptedException {
    byte b = -1;
    // 一个字节 -1,输出的二进制是 1111 1111,正是 -1 机器数 1000 0001的补码
    System.out.println(getBinary(b));
    System.out.println(b);
}

// 获取一个字节的二进制表示
private static String getBinary(byte b) {
    byte[] array = new byte[8];
    for (int i = 0; i < 8; i++) {
        array[7 - i] = (b & (1 << i)) == (1 << i) ? (byte) 1 : 0;
    }
    return Arrays.toString(array);
}

问题解析

再回到最初的那个问题,很明显,计算逻辑是正确的,但为什么没有得到正确的值呢?

byte[] array = readSerialBytes();
int result = (array[0] << 8) + array[1];

问题就在于符号位,在Java中,每种基本数据类型都具有固定的字节长度,均以最高位作为符号位,在进行计算时必须确保符号位始终发挥正确的作用。

很明显对字节array[0]array[1]来说,它们都是最终值的一部分,是不应该有符号位的,或者说它们的符号位应该作为数据位参与计算,最终结果以int值表示。

int result = ((array[0] & 0xff) << 8) + (array[1] & 0xff)

byte & 0xff的作用,可以这样解释,0xff是一个Int值,低字节即低8位都是二进制1ByteInt进行操作,会自动扩展为Int,但实际只有低8位是有效的,其它位包括最高的符号位,都必须置为0。所以,这样做就将字节转换为无符号数,并扩展为Int,这样再进行移位操作就能得到正确的数值了。

至于为什么会出现-32639这个数呢,下面就是答案:

int result = (array[0] << 8) + array[1];
           = 1000 0001 0000 0000(补码) + 1000 0000 1000 0001(补码)
           = 1000 0000 1000 0001(补码)
           = 1111 1111 0111 1111(原码)
           = -32639(真值)
arrow_upward