字节的原码、补码与反码

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

比如从串口中收到一个长度为 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 Exception {
    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