起因
写这篇文章的起因是在某个地方需要将字节数组byte[]转16进制数字int。见上一篇文章: 进制转换的一些内容,我写出来的方法长这样。
byte[] bytes = new byte[]{35,35};
//方法1:10进制byte转16进制字符串,16进制字符串转16进制数字int
public int bytesToInt(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder("");
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i] & 0xff;
String str = Integer.toHexString(b);
if (str.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(str);
}
return Integer.parseInt(stringBuilder.toString(),16);
}
当然,功能是实现了,但是直到我看见大佬写的方法,长这样:
byte[] bytes = new byte[]{35,35};
//方法2:位运算直接转(数组长度必须小于等于4)
public int bytesToInt(byte[] bytes) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
int value = 0;
for (int i = 0; i < bytes.length; i++) {
value = (value << 8) | (buffer.get() & 0xFF);
}
return value;
}
大佬的方法如此简洁,但运行之后能产生同样的效果。对位运算并不熟悉的我看得有点懵逼,这就忍不住去复习了一遍位运算的内容:
位运算符
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都为1时,结果才为1 |
| | 或 | 两个位都为0时,结果才为0 |
^ | 异或 | 两个位相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
重点看大佬代码中用到的<< 8
和& 0xFF
分别是什么意思,为什么简单两个运算符就能达成我代码中又是创建变量又是计算好几行的效果。
1.Java中^ =
运算符的目的
用于删除byte数组中的重复项
例如:一个byte数组[1, 1, 2, 2, 3, 3, 4]
进行遍历^ =运算,结果是4,也就是把前面两两相等的数字都清除掉了,留下没有可以配对两两抵消的数字4。
byte[] bytes = new byte[]{1, 1, 2, 2, 3, 3, 4};
byte checkCode = 0x00;
for (byte bt : bytes) {
checkCode ^= bt;
}
System.out.println(checkCode);//4
2.Java中& 0xff
运算符的目的
&:表示按位与,只有两个位同时为1,才能得到1,例如:
//0b开头用来表示这是一个2进制数字
int a = 0b0000 0000 1111 1111 0000 0000 1000 0001;
int b = 0b0000 1111 0000 1111 0000 0000 1000 0001;
//上下相与,同时为1才得1,否则就为0
a & b = 0b0000 0000 0000 1111 0000 0000 1000 0001;
0xff转为2进制是:0000 0000 0000 0000 0000 0000 1111 1111
所以:& 0xff是一种二进制的位运算,表示只取后8位的数字。
当一个byte会转换成int时,由于int是32位,而byte只有8位这时会按照符号进行补位,例如:
byte b = -127;//1000 0001
int a = b;//1111 1111 1111 1111 1111 1111 1000 0001
// 0xff:0000 0000 0000 0000 0000 0000 1111 1111,上下相与,只保留后8位,也就是1000 0001,前面都置0
System.out.println(a);//-127
a = b&0xff;//0000 0000 0000 0000 0000 0000 1000 0001
System.out.println(a);//129
可以看到:byte会自动补位,把前24位都补为1。
所以:当byte转为int类型数据的时候,就需要&0xff计算,只保留后8位数字,前面24位都置0
3.Java中<< 8
运算符的目的
<<:将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
在上面的byte[]数组转String的例子中:假如我们有一个byte数组[35, 36, 37, 38, 39, 40]。现在需要将这四位转成16进制的int(或者long)并且拼接到一起。例如[35,36]转换之后是[23,24],那么需要输出的结果是2324。
@Test
void demo11() {
byte[] bytes = new byte[]{35, 36, 37, 38, 39, 40};
ByteBuffer buffer = ByteBuffer.wrap(bytes);
int value = 0;
for (int i = 0; i < bytes.length; i++) {
value = (value << 8) | (buffer.get() & 0xFF);
log.info("第 {} 次计算", i);
log.info("10进制value {}", value);
log.info("16进制value {}", Long.toHexString(value));
}
}
@Test
void demo12(){
byte[] bytes = new byte[]{35, 36, 37, 38, 39, 40};
StringBuilder stringBuilder = new StringBuilder("");
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i] & 0xff;
String str = Integer.toHexString(b);
if (str.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(str);
log.info("第 {} 次计算", i);
log.info("10进制value {}", Long.parseLong(stringBuilder.toString(),16));
log.info("16进制value {}", stringBuilder);
}
}
在上面的例子demo11中,就利用了位运算来完成这一工作,避免了创建字符串对象并反复追加字符串的工作。
在这里,每次取得的byte都执行<<8 | 下一个byte的低8位,换成二进制语言来解释,如下表所示:
原始int value=0;
原始byte数组:
byte(10进制) | 35 | 36 | 37 | 38 | 39 | 40 |
---|---|---|---|---|---|---|
转换成2进制 | 0010 0011 | 0010 0100 | 0010 0101 | 0010 0110 | 0010 0111 | 0010 1000 |
转换成16进制 | 23 | 24 | 25 | 26 | 27 | 28 |
2进制计算过程:
计算过程 | 计算结果value | 十进制 | 十六进制 | |
---|---|---|---|---|
value = (value << 8) | (buffer.get() & 0xFF); 即:value左移8位,与 byte的低8位进行或运算 | ||||
第1次执行 | 0 | 0010 0011 | 0010 0011 | 35 | 23 |
第2次执行 | 0010 0011 0000 0000 | 0010 0100 | 0010 0011 0010 0100 | 8996 | 2324 |
第3次执行 | 0010 0011 0010 0100 0000 0000 | 0010 0101 | 0010 0011 0010 0100 0010 0101 | 2303013 | 232425 |
第4次执行 | 0010 0011 0010 0100 0010 0101 0000 0000 | 0010 0110 | 0010 0011 0010 0100 0010 0101 0010 0110 | 589571366 | 23242526 |
第5次执行 | 0010 0100 0010 0101 0010 0110 0000 0000 | 0010 0111 | 0010 0100 0010 0101 0010 0110 0010 0111 | 606414375 | 24252627 |
第6次执行 | 0010 0101 0010 0110 0010 0111 0000 0000 | 0010 1000 | 0010 0101 0010 0110 0010 0111 0010 1000 | 623257384 | 25262728 |
可以看到结果与通过String变量每次获取一个byte的16进制之后再拼接起来的结果一致,但存在一个问题:
- Java中的int类型存储的二进制数字上限是32位,也就是左移8位这个操作只能执行3次,第四次执行的话就会覆盖第一次取得的8位2进制数字。
所以采用位运算来将byte转换成10进制的int,局限性在于只能转换length<=4的byte[]数组,length一旦超过4,就只能取到byte[]数组最后的4位转换之后的结果。
如果把value的类型改成long,long类型最多可以存储64位的数字,那么位运算的大小限制就扩大了一倍,也就是,左移8位这个操作可以执行7次,即,length的大小最多为8。
而字符串拼接法在这一点上,只需要引入BigInteger,转换对于数据的长度是没有限制的。
综上所述:
在byte[]数组转10进制数字的时候,有两种方法:分别是位运算转换法和字符串拼接转换法:
- 位运算转换:性能更优,但对于数组长度有限制,数组长度必须<=8,如果长度超过8,就只转换最后8位。
- 字符串拼接转换:性能稍劣,但对于数组长度无限制。
因此在需要转换的byte[]数组长度小于等于8的情况下,用位运算更佳;长度大于8,则必须用字符串拼接法转换。
//样例数据
private static byte[] bytes8 = new byte[]{18,52,86,120,-112,18,52,86};//8位
private static byte[] bytes15 = new byte[]{18,52,86,120,-112,18,52,86,120,-112,18,52,86,120,-112};//15位
/**
* 位运算转换法
*/
@Test
void demo1() {
ByteBuffer buffer = ByteBuffer.wrap(bytes8);
long value = 0;
for (int i = 0; i < bytes.length; i++) {
value = (value << 8) | (buffer.get() & 0xFF);
log.info("第 {} 次计算", i);
log.info("10进制value {}", value);
log.info("16进制value {}", Long.toHexString(value));
}
}
/**
* 字符串拼接转换法
*/
@Test
void demo2() {
StringBuilder stringBuilder = new StringBuilder("");
for (int i = 0; i < bytes8.length; i++) {
int b = bytes[i] & 0xff;
String str = Integer.toHexString(b);
if (str.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(str);
log.info("第 {} 次计算", i);
BigInteger bigInteger = new BigInteger(stringBuilder.toString(), 16);
log.info("10进制value {}", bigInteger.toString(10));
log.info("16进制value {}", stringBuilder);
}
}
- bytes8运行结果:位运算转换法和字符串拼接转换法两种方法运行结果一致
- bytes15运行结果:
位运算:
字符串拼接: