字节的原码、补码与反码
立泉近期项目中经常需要处理串口数据的解析,在将收到的字节进行校验、计算、转换时遇到了一些问题。其实只要了解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(十进制)
理论上计算结果应该是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 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位
都是二进制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(真值)