补码——Java类型byte强转short发现的坑

3,562 阅读2分钟

背景

做音频开发的同学一般会和byte数组打交道比较多,因为PCM原始数据一般都是byte数组来表示,如果音频的位深是16bit,那就会是连续两个byte元素表示一个音频幅值。如果需要对幅值进行计算,那就要先将两个byte还原回short值(16位数)再进行计算。
我的byte数组是小端存储,于是我想当然的认为short值计算的Java代码应该是:

//2个byte转化为1个short
//将高字节填充到short的高8位,低字节填充到short的低8位
short data = bytes[0] + (bytes[1] << 8);

运行结果发现,在两个byte都是正数的情况下无问题,任一个是负数或者两个负数的情况下,会出现高8位数据减去了1的情况。比如:

0xb1 + (0x04 << 8) 会得到值 0x03b1,而不是 0x04b1

原因

补码

学计算机组成原理的时候,相信大家都对“补码”一词有概念。这个问题正是补码引起的。
CPU里只有加法器(ALU),没有减法器,因为可以用补码将减法变为加法。
原码和补码的关系:

正数: 补码和原码一致<br>
负数: 原码的符号位不变,其他位取反加1就是补码
如:
-1的原码(8bit) : 1000 0001 
-1的补码(8bit) : 1111 1111 (即0xff)
所以,做个最简答的减法 1 - 1 
得:1 - 1 = 1 + (-1) = 0x01 + 0xff = 0
可见,利用补码和溢出的方式,可以很巧妙的将减法转化为加法

强转

强转会发生什么,运行如下代码就会知道。

byte b = -1;            //8位的-1:0xff
short s = (short) b;    //16位的-1: 0xffff

所以,负数的byte强转为short时,高八位会全补上1,以保证强转后数据的值不发生变化,但是如果遇上两个byte来表示short的情况,就引发了背景中出现的问题

0xb1 + (0x04 << 8) = 0xffb1 + 0x0400 = 0x03b1

解决方式

相信大家也见过很多博客中这样子的代码

byte b = -1;
short s = (short) b & 0x00ff;

这样可以保证补的高8位一定是0,而不是1。我们再用这个方法来算一下:

(0xb1 & 0x00ff) + (0x04 << 8) = 0x00b1 + 0x0400 = 0x04b1

这样就完美将值进行了还原
当然,Java内对于byte数组读short值,有更专业的解决方法:

byte[] data = new byte[2];
data[0] = 0xb1;
data[1] = 0x04;
short s = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).getShort(0);

可以得到正确的short值