在实际工作中,如果我们用的语言存在跨语言的情况,那么我们无形中可能会遇到各种各样,奇奇怪怪的坑,比如说加解密算法的使用,数据类型的转换等。今天就来给大家分享下,两种语言中的byte
类型所带来的坑。
背景
因工作需要,我们需要通过PC电脑和安卓手机做网络通信。因TCP
的通信过程中会存在粘包问题,所以我们在通信的过程中,需要指定传输的字节长度,这个时候我们就需要处理数据类型转换的问题了。通常我们传输数据都是使用字节数组(byte[]
)来承载我们的数据,而我们传输过程中需要带上长度的信息,这个时候就需要把长度值(int
类型),转成byte[]
,以方便被传输。
过程
C#代码:int 转 byte[]
/// <summary>
/// 将int类型以数组方式添加到目标数组
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="value"></param>
public static void IntoBytes(byte[] data, int offset, int value)
{
data[offset++] = (byte)(value);
data[offset++] = (byte)(value >> 8);
data[offset++] = (byte)(value >> 16);
data[offset] = (byte)(value >> 24);
}
C#代码:byte[] 转 int
/// <summary>
/// bytes数据长度转成int类型
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <returns></returns>
public static int ToInt32(byte[] data, int offset)
{
return (int)(data[offset++] | data[offset++] << 8 | data[offset++] << 16 | data[offset] << 24);
}
以上代码,将一个int
类型的数据转成byte[]
后,也可以正常的转回来。为了投机取巧,java
端并没有对这段代码做处理,直接拿去使用。而在实际使用中,java
传递过来的长度并没有问题,能够正常解析。然后从C#
传递给java
的数据,却发现解析的长度值和发送的长度值相差非常大。比如:byte[]{178,24,0,0}
,转换后的值为-78
。字节长度为负数,显然是错的,而且错的很离谱。
为什么值会是负数呢?
原因
在java
中,byte
的取值范围是【-128 127】
;而在C#
中,byte
的取值范围是【0 255】
!
是的,正是因为byte
数据类型,在两种不同的语言中,定义的存储范围存在差异。C#
代码中的ToInt
方法,是以正数的方式来做解析,然而java
中的byte
存在负数,所以结局当然也可想而知了。
科普
二进制在存储负数时,是通过最高位来做记录的。
1
表示负数,0
表示正数。当一个数字存在负数时,先是以绝对值的方式来存储,再将字节取反后+1
。如:7
的二进制表示:0 0000 111
,负数的话便是:1 1111 000 + 1 = 11111001
(第一个1表示负数,1111 000
表示取反的值)。
分析
知道上面的背景知识以后,我们再来分析下我们应该要怎么解析以上二进制数据。
以C#的存储方式来分析:
一个int
类型是4
个字节,占4*8=32
位,也就是:11111111
1111111
1111111
1111111
,也就是每8
个位代表着一个字节,下面我们以C#中的byte[]{178,24,0,0}
为例:
助算:2^1=2 2^2=4 2^3=8 2^4=16 2^5=32 2^6=64 2^7=128
十进制 | 二进制 | 类型 | 语言 |
---|---|---|---|
178 |
10110010 |
byte |
C# |
24 |
00100010 |
byte |
C# |
0 |
00000000 |
byte |
C# |
0 |
00000000 |
byte |
C# |
解析
178
:2^7 + 2^5 + 2^4 + 2^1 = 178
,取第8,6,5,2
位为1
,其余补0
。最终取值为10110010
。
24 :2^4 + 2^3 = 24
,取第5,4
位标记为1
,其余补0
。最终取值为00100010
。
byte[]
二进制表示为:10110010 00011000 00000000 00000000
特别注意:byte[]
数组中的字节高位是在后面的,低位是在前面的,所以对应int类型二进制表示为:"00000000 00000000 00011000 10110010"
即:6322
。
以java的存储方式来分析:
int
类型6322
转成byte[]
后,在java中的表示形式为:{-78,24,0,0}
,在这里要使用前面提到的负数的表示方法。
十进制 | 二进制 | 类型 | 语言 |
---|---|---|---|
-78 |
10110010 |
byte |
java |
24 |
00100010 |
byte |
java |
0 |
00000000 |
byte |
java |
0 |
00000000 |
byte |
java |
解析
十进制-78
:10110010
在java中表示的为负数,所以我们需要反过来取值:10110010
减1
=>10110001
,再取反=>01001110
,即:78
。
根据上面分析,各字节对齐思路没有问题,java
和C#
的二进制是一致的。
解决问题
问题清楚了,在java
中主要是需要解决负数表示的问题,因为负数的补位和正数是不一样的。如果是左移<<
,那么就是用0
补位。如果是右移>>
,那么就是用1
补位。正因为如此,才导致了移位运算后,结果不符合预期。
所以我们需要加上0xFF
与运算,用0xFF
做运算的主要目的是取高位,让byte
负数的表示,还原成int
的正数表示。具体代码如下:
/// <summary>
/// bytes数据长度转成int类型
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <returns></returns>
public static int ToInt32(byte[] data, int offset)
{
return (int)((data[offset++] & 0xFF) | ((data[offset++] & 0xFF) << 8) | ((data[offset++] & 0xFF) << 16) | ((data[offset] & 0xFF) << 24));
}
说明:&
运算表示相同位部分,如果都为1
则取1
,否则取0
本文会经常更新,请阅读原文: https://huchengv5.gitee.io//post/java%E4%B8%8EC-%E5%A4%84%E7%90%86byte%E7%B1%BB%E5%9E%8B%E5%B8%A6%E6%9D%A5%E7%9A%84%E5%9D%91.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名胡承(包含链接: https://huchengv5.gitee.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。