前言

byte这个单词是Java八种基本数据类型之一字节的关键字,在计算机存储中以字节为单位,8位比特(bit)组成一个字节。
为什么弄清楚byte这么重要呢?因为智能硬件的数据传输大部分协议都是按字节一位一位来解析的,对于字节的运算十分频繁,如果不对byte研究透彻,就很容易犯一些特别基础的错误。

1.取值范围

byte由8位bit组成,每个bit只能是0或者1,所以byte一共有2的8次方种可能,也就是256。

这样来说byte的取值范围就应该是0~256了吧,但是在Java中有一点需要特别注意,除了boolean和char是无符号的,其余的基本数据类型都是有符号的。而符号要怎么表示的,答案就是用最高位来标识,0为正数,1为负数。

若byte为正数,则其最大值为

0111 1111 //转换为10进制,2^6+2^5+2^4+2^3+2^2+2^1+1=127

若byte为负数,则其最大值为

1111 1111 //-127

故byte的取值范围是-127~127。

但这个结论与我们在各种Java教程中看到的取值范围不吻合,byte的真正取值范围是

-2^8~2^8-1 即是 -128~127

这个多出来的-128是怎么用字节表示的呢,用下面代码打印出-128的二进制表示。

byte b1 = -128;
System.out.println(Integer.toBinaryString(b1));
//结果
11111111 11111111 11111111 10000000

截取最后8位,所以-128的二进制表示为10000000,从表面上看就是-0。

实际上我们将二进制1111 1111表示的数字打印出来也不是-127,而是-1,可用下面代码进行验证

 byte b1 = (byte) 0B11111111;
System.out.println(b1);
//结果
-1

出现这种情形是因为计算机用补码的方式存储数值,关于补码详细介绍可参考这篇文章

简而言之就是,正数的补码是自身,而负数的补码则是其绝对值按位取反再加1

要注意-128是不能用常规的方式转换成补码的,只是规定了10000000这个值用来表示-128。

2.为何赋值时会经常报错?

在编码过程中,经常会遇到给byte变量赋值时报错,提示的错误如下

byte b1 = 0x8F;
//报错提示
Incompatible types
Required:byte
Found:int

意思就是类型不兼容,需要的是byte型,但给的是int型。int型的数值范围比byte大,会导致大于127的值无法正常表示,所以会报错。解决报错的方法就是将等号右边的字面值强转为byte。

byte b1 = (byte) 0x8F; //-113

在Java中,所有整型字面值都是int型,所有小数都是double型

在上述的错误中0x8F被当成int型,并且其未带负号,转换成10进制为143。而byte的取值范围是-128~127,明显小于143,数值超出了byte的最大值,所以编辑器会报错,只能用强转的方式将int的最后8位截取掉变成byte类型。

对byte的赋值几乎都是采用16进制表示的,如上述的0x8F,在很多情况下byte类型的变量并不是想看其十进制的值,而是想知道每一个bit的值,用二进制表示显得过长,而用16进制表示一个byte只需要两位即可,每一位表示4个bit。

什么情况下byte的赋值会报错提示类型不对呢?那就是当数值大于127,换句话说就是byte的最高位为1时,在16进制里面就是当第1位大于7时就会报错。

byte b1 = 0x7F; //不报错,无须强转
byte b2 = 0x80; //报错,必须要强转

3.基本四则运算注意点

首先来看一个byte运算的例子

byte b1 = 0x20;
byte b2 = 0x10;
byte b3 = b1 - b2; //报错
//正确 byte b3 = byte(b1-b2);

我们会发现最后一句代码报错,提示也是类型不兼容,需要的是byte,而给的int。从表面上看b1-b2的值为16,绝对没有超过127,为何也会报错呢?

原因就在于Java的运算机制,在Java中两个变量只要参与了数学运算,就有可能会进行类型提升,只要类型比int小(即byte,char,short),那么在运算之前,这些值会自动转换成int。

通常表达式中出现的最大的数据类型决定了表达式最终结果的数据类型。如果将一个float值与一个double值相乘,结果就是double,如果将一个int和一个long值相加,则结果为long。

用byte运算要注意,如果结果的类型也是byte,有可能会发生负数运算后成正数,正数运算后变成负数的情况。看下面的例子:

byte b4 = (byte) 0xA1;
byte b5 = (byte) (b4*2);
System.out.println("b4="+b4+",b5="+b5);
//结果
b4=-95,b5=66

byte b6 = 0x46;//十进制为70
b6*=2; //预期结果应为140
System.out.println("b6="+b6);
//结果
b6=-116

b4值为-95,乘以2得到-190,因为运算时会转换成int类型,-190的二进制为

1111 1111 1111 1111 1111 1111 0100 0010

再对-190进行强转,截取最后8位变成byte,最后结果的二进制为

0100 0010  //66

b6的值为0x46,转换成十进制为70,乘以2结果为140,二进制表示为

00000000 00000000 00000000 10001100

强转后结果为1000 1100,由于最高位是1,则说明值已经是个负数了。可怎么看起来这个值不像是-116的,而应该像-12的。这就涉及到补码了,这个二进制只是在计算机中的存储表示,要经过转换方能得到真值。转换过程如下:

  • 最高符号位不变,其余位全部取反,得到1111 0011
  • 再加1,得到1111 0100
  • 符号位不计算,其余7位转换为10进制的值是 64+32+16+4=116
  • 再加上负号,最终结果为-116。

最后结论是用byte运算并且结果也是byte时要注意结果的符号可能会翻转,当计算的值大于127或者是小于-127时符号就会翻转,翻转后的值与真正的值两者的绝对值之和必定是256。

4.位操作运算

Java中的位操作有4种,分别是与&,或|,非~,异或^,位操作就是指对每一个bit进行操作,操作时将数据用二进制表示会更加直观,下面是位操作的运算规则说明。

操作符值1值2结果
&010
|011
~NA10
^011

byte类型在位操作运算时都会转成int类型,运算的结果也是int类型。

在许多通信协议中会看到最后一位字节是校验位,校验值也分为很多算法,常见的有求和与异或校验,假如求和的值超过了byte的最大值,这种情况下会造成校验不准吗?

答案是不会,假设在C中计算的校验和是unsigned char类型,累加校验值假定为200,转换成二进制就是11001000。在Java中计算时,因为byte会转换成int类型,所以计算的结果200用二进制表示就是

00000000 00000000 00000000 11001000

强转成byte为1100 1000,十进制为-56,虽然看起来和C中的值符号不一致,但在二进制中两者的每一位都是相同的,实际上两者在Java中就是相等的,所以累加和之类的校验可以计算出前几位的和并强转成byte之后直接与最后一位的校验值比较是否相等。

虽然校验值可以不用转换直接对比,但是其他的数据字节计算就需要转换了。最常见的就是单片机用串口发送不了浮点数据过来,只能发一个字节的整型,所以普遍的做法就是发两个字节代表浮点数,高位与低位,计算公式为

(高位字节*256 + 低位)/100

很明显这个高低位字节在C中就是无符号char,是不会出现负数的。

假如需要发送599.21的浮点值,那么高位值就为0xEA,低位值为0x11,在Java中用这个公式计算得到的结果却是一个负数。

b1 = (byte) 0xEA;
b2 = 0x11;
float f1 =( b1 * 256 + b2)/100F;
System.out.println("f1="+f1);
//结果
f1=-56.15

在这种情况下,高位和低位的字节都必须先转换成正数再去进行计算。Java中的byte转换成正数的方式如下。

b1 = (byte) 0xEA;
System.out.println("b1="+b1);
int i1 = b1 & 0xFF;
System.out.println("i1="+i1);
//结果
b1=-22
i1=234

切忌使用b1&=0xFF这种方式进行转换,byte想要转换成正数必须要提升类型,所以这里使用了int类型来保存与0xFF位与运算后的结果。

用这种转换方法再来计算一遍上述的浮点值,就能得到正确的结果

b1 = (byte) 0xEA;
b2 = 0x11;
float f1 =( (b1&0xFF) * 256 + (b2&0xFF))/100F;
System.out.println("f1="+f1);
//结果
f1=599.21

在串口通信中,还有另外一种操作是经常使用的,那就是取某几个bit的值,这就需要用到移位操作了。

假如有一个字节为0x9A,需要取最高两位的值,取值方法如下:

//0B1001 0110 最高位为10,值应为2
b1 = (byte) 0x96;
b1 = (byte) ((b1 & 0xFF)>>6);
System.out.println("b1="+b1);
//结果
b1=2

在移位之前一定要跟0xFF相与转成正数,避免在移位时有1的值移到前8位里面造成结果错误。

注:称位操作在Java中有3种,

  • 左移<<,低位补零
  • 右移>>,若符号为正则在高位插入0,符号为负则在高位插入1
  • 无符号右移>>>,无论正负,都在高位插入0
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐