字节的原码、补码与反码
立泉近期项目经常处理串口数据,在对收到的字节进行校验、计算、转换时遇到一些问题。其实只要理解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(十进制)
理论上计算结果应该是0x8181
即33153
,但按上面的计算方法结果却是-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 位都是二进制1
,Byte
与Int
进行与计算会自动扩展为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(真值)