一、前言
平时的App应用层开发中,很少和byte打交道,最近刚做了一个和硬件设备交互的产品,通信使用UDP,交互协议就是自己定义的协议,每个数据包中分别定义不同的字节数来代表不同的意义,比如:
字节号 | 信息内容 | 字节数 | 码值 | 说明 |
---|---|---|---|---|
1-2 | 数据头 | 2 | 0x3e8 | 0x3e8 |
3 | 工作状态 | 1 | bit0-bit7 | 0x00:待机;0x01:发射 |
… | … | … | … | … |
要了解这块内容,必须要了解二进。字节是二进制数据的单位,一个字节通常为8位长,其中位(bit)使用的0或1表示,通常情况下byte都是无符号的,所以取值范围为0~255。
二、机器数
机器数是将符号“数字化”的数,是数字在计算机中的二级制表示形式。
数的符号数字化
现实中的数有正数和负数,由于计算机内部的硬件只能表示两种物理状态(0和1表示),因此数的正号“+”和负号“-”,在机器里用一位二进制的0或者1来区别。通常这个符号放在二进制数的最高位,称为符号位,0代表正数,1代表负数。
真值
带符号位的机器数的数值称为机器数的真值。比如
二进制 0000 0001 的真值为1;
二进制 1000 0000 的真值为-1;
三、原码、反码、补码的概率和表示方法
对于一个数,计算机要使用一定的编码方式进行存储。原码、反码、补码就是机器存储一个具体数字的编码方式。
3.1 原码
原码就是符号位加上真值的绝对值,即用第一位表示符号位,其余位表示值。
真值[3] = 原码[0000 0011]
真值[-3] = 原码[1000 0011]
因为第一位是符号号,所以8位二进制的原码取值范围(10进制表示)就是:
[1111 1111,0111 1111]即[-127,127]
原码是人脑最容易理解和计算的方式。
3.2 反码
在了解反码之前我们线看下面的运算:
3 + 3:
原码[0000 0011] + 原码[0000 0011] = 原码[0000 0110] = 真值[6]
正数的加法运算在上面的例子中使用原码进行计算是没有问题的,那么我们来看一下减法运算呢:
3 - 3:在计算机中,为了简化设计,将符号位也参与运算,因此减法可以转换为加法运算,这样计算机的运算设计就更简单了:即
3 - 3 = 3 + (-3) 转化为原码进行计算则为:
原码[0000 0011] - 原码[0000 0011] = 原码[0000 0011] + 原码[1000 0011] = 原码[1000 0110] = 真值[-6]
显然上面的计算结果是不对的。
为了解决减法的问题,出现了反码:
反码表示方法:
- 正数的反码是其本身
- 负数的反码在其原码的基础上,符号位不变,其余各个位取反
例:
[+3] = 原码[0000 0011] = 反码[0000 0011]
[ -3] = 原码[1000 0011] = 反码[1111 1100]
下面我们使用反码来计算3-3:
3 - 3 = 原码[0000 0011] + 原码[1000 0011] = 反码[0000 0011] + 反码[1111 1100] = 反码[1111 1111] = 原码[1000 0000] = 真值[-0]
使用值的反码进行计算,结果中的真值正确了,但是有个符号-0,虽然人们在理解上+0和-0是一样的,但0带符号是没有任何意义的。而且会有[0000 0000]原和[1000 0000]原两个编码表示0。
于是补码出现了,为了解决0的符号以及两个编码的问题:
3.3 补码
补码表示方法:
- 正数的补码就是其本身
- 负数的补码在其原码的基础上,符号位不变,其余各个位取反,最后+1,即(在反码的基础上+1)
例:
[+3] = 原码[0000 0011] = 反码[0000 0011] = 补码[0000 0011]
[ -3] = 原码[1000 0011] = 反码[1111 1100] = 补码[1111 1101]
下面我们使用补码来计算3-3:
3 - 3 = 原码[0000 0011] + 原码[1000 0011] = 反码[0000 0011] + 反码[1111 1100] =补码[0000 0011] + 补码[1111 1101] = 补码[0000 0000] = 反码[0000 0000] = 原码[0000 0000] = 真值[+0]
再计算一下3 - 4:
3 - 4 = 原码[0000 0011] + 原码[1000 0100] = 反码[0000 0011] + 反码[1111 1011] =补码[0000 0011] + 补码[1111 1100] = 补码[1111 1111] = 反码[1111 1110] = 原码[1000 0001] = 真值[-1]
从上门的例子中,可以看出使用补码进行计算,解决了使用反码进行计算时0的符号问题以及两个编码问题,这样可以使用[00000000]表示0,而且还可以使用[10000000]表示-128:
(-1) + (-127) = 补码[1000 0001] + 补码[1111 1111] = 补码[1111 1111]补 + 补码[1000 0001] = 补码[1000 0000]
-1-127的结果应该是-128,在补码运算中,[1000 0000]补就是-128. 但是需要注意,这里使用以前的-0补码来表示-128,所以-128没有原码和反码表示
因此使用补码,不仅修复了0的符号以及存在两个编码的问题,还能够多表示一个最低数,这就是为什么8位的二进制,使用原码或者反码的表示范围是[-127,127],而使用补码表示的范围是[-128,127]。
因为机器使用的是补码,所以对于编程中常用到的32位int类型,可表示的范围是[-2^31, 2^31-1]
四、了解java中的byte表示方式
通过上面的描述,不同的编码的取值范围是不一样的,下面我们来验证一下java中到底原码、反码还是补码表示。
1,验证byte的取值范围
System.out.println(Byte.MAX_VALUE);
System.out.println(Byte.MIN_VALUE);
输出结果为:
127
-128
由此可见,Java中使用的是补码,只有使用补码,才能有最小值为-128
2,查看二进制编码
String s1 = String.format("%8s", Integer.toBinaryString(127 & 0xFF)).replace(' ', '0');
String s2 = String.format("%8s", Integer.toBinaryString(-128 & 0xFF)).replace(' ', '0');
String s3 = String.format("%8s", Integer.toBinaryString(-1 & 0xFF)).replace(' ', '0');
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
输出结果为:
01111111
10000000
11111111
五、Java中向流中读取获取写入字节
5.1 Java中写入字节
字节号 | 信息内容 | 字节数 | 码值 | 说明 |
---|---|---|---|---|
1-2 | 数据头 | 2 | 0x3e8 | 0x3e8 |
3 | 工作状态 | 1 | bit0-bit7 | 0x00:待机;0x01:发射 |
4 | 量程 | 1 | bit0-bit7 | 0x00:1km;0x01:2km |
5 | 温度 | 1 | bit0-bit8 | -128-127度 |
6 | 转速 | 1 | bit0-bit8 | 0-255;无符号 |
… | … | … | … | … |
字节的写入,这里我们使用DataOutputStream,这个类里面封装了相关的类型的写入操作:
如下所示:
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream outputStream = new DataOutputStream(byteArrayOutputStream);
//数据头2个字节可以用short表示
outputStream.writeShort(0x3e8);
//工作状态一个字节
outputStream.writeByte(0x01);
//量程
outputStream.writeByte(0x01);
//温度
outputStream.writeByte(100);
//转速
outputStream.writeByte(255);
//其他待写入的类容......
byte[] outBytes = byteArrayOutputStream.toByteArray();
} catch (Exception ex) {
ex.printStackTrace();
}
5.2 Java中按照字节数读取内容
读取字节这里使用DataInputStream,也封装了相关类型的读取操作:
//读取内容
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(outBytes);
DataInputStream inputStream = new DataInputStream(byteArrayInputStream);
//数据头
System.out.println(inputStream.readShort());
//工作状态
System.out.println(inputStream.readByte());
//量程
System.out.println(inputStream.readByte());
//温度
System.out.println(inputStream.readByte());
//转速 0-255,Byte的有符号最大值是127,所以这要使用无符号的byte取法
System.out.println(inputStream.readUnsignedByte());
输出结果为:
1000
1
1
100
255
注意:
这里在进行协议读取的时候一定要看清每个字节的文档说明,不然有可能造成读取的数据不对,比如这里的转速是无符号的,
//转速
outputStream.writeByte(255);
如果在读取没有注意这个,直接使用
System.out.println(inputStream.readByte());
得到的结果为-1,真确的应该读取无符号的byte,这样才能正确的255;
最后
原码、反码、补码的出现以及表示方法上面已经描述过了,当然了解这些的目的就是为了使用,因此后面出现了byte在Java中的相关操作,特别是和硬件通信的时候,大部分我们都定义一些指定的协议来完成。