在实际工作中,如果我们用的语言存在跨语言的情况,那么我们无形中可能会遇到各种各样,奇奇怪怪的坑,比如说加解密算法的使用,数据类型的转换等。今天就来给大家分享下,两种语言中的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#

解析
1782^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

解析
十进制-7810110010在java中表示的为负数,所以我们需要反过来取值:101100101 => 10110001,再取反=> 01001110,即:78

根据上面分析,各字节对齐思路没有问题,javaC#的二进制是一致的。

解决问题

问题清楚了,在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/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系