Java使用ByteBuffer处理大小端踩坑

Java使用ByteBuffer处理大小端踩坑


建议大家使用netty的ByteBuf,JDK这个是一坨屎,不必研究

1. 什么是大小端

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;

小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。

大小端示例

  1. JVM默认为大端模式
  2. ByteOrder.nativeOrder()方法返回的是本地操作系统默认的处理方式,与JVM无关

2 踩坑

问题:01 02 03 (16进制),现在假设该数据类型为大端模式,如何得到该数据的小端模式?

答案:根据定义该数据的小端模式应为 03 02 01

好了,这么简单的问题,花1分钟了解概念,1秒钟判定Java肯定自带工具包转换,再花1分钟找到ByteBuffer

为自己的傲慢开始买单:

假如这样写:

 ByteBuffer bb = ByteBuffer.allocate(8);
 byte[] bytes = Longs.toByteArray(Long.valueOf("010203", 16));
 bb.put(bytes);
 bb.order(ByteOrder.LITTLE_ENDIAN);
 System.out.println(Arrays.toString(bb.array()));

输出:

[0, 0, 0, 0, 0, 1, 2, 3]

卧槽?啥玩意,不是应该 [3, 2, 1, 0, 0, 0, 0, 0] 的吗???
看看按字节读取

  for (int i = 0; i < 8; i++) {
     System.out.print(bb.get() + " ");
  }

输出:

0 0 0 0 0 1 2 3 

没变化,也对,毕竟大小端转换是以字节为基本单位的,肯定没变化,压根没转呗。
试试直接读个Long?

 System.out.println(Arrays.toString(Longs.toByteArray(bb.getLong())));

输出:

[3, 2, 1, 0, 0, 0, 0, 0]

转了!行8,二话不说撸源码看看!

//bb.get()是单字节读取,所以不会有任何改变
public byte get() {
    return hb[ix(nextGetIndex())];
}

//bb.array()也不会变化,因为原始字节数组根本没有变化
public final byte[] array() {
    if (hb == null)
        throw new UnsupportedOperationException();
    if (isReadOnly)
        throw new ReadOnlyBufferException();
    return hb;
}
//getLong就涉及到了大小端转换
public long getLong() {
    return Bits.getLong(this, ix(nextGetIndex(8)), bigEndian);
}

果然这俩方法都是直接返回的,不涉及大小端,getLong方法就涉及到了大小端转换,所以成功了!

小结:
这样写的话ByteBuff只是以小端模式读取,此时改变的是读取时数据类型的字节数据,
注意,原始字节数组此时根本没有变化,因为在put之前没有设置大小端模式。

那么问题来了,我要是在put之前设置了小端呢?

 ByteBuffer bb = ByteBuffer.allocate(8);
 byte[] bytes = Longs.toByteArray(Long.valueOf("010203", 16));
 bb.order(ByteOrder.LITTLE_ENDIAN);
 bb.put(bytes);
 System.out.println(Arrays.toString(bb.array()));

输出:

[0, 0, 0, 0, 0, 1, 2, 3]

没变化!因为是以字节为单位put进去的本身就是一个字节数组,ByteBuffer此时是以字节为数据类型的,单字节不涉及大小端问题,所以并没有变化!

改变方案,short类型

  ByteBuffer bb = ByteBuffer.allocate(6);
  short[] bytes = {1, 2, 3};
  bb.order(ByteOrder.LITTLE_ENDIAN);
  bb.asShortBuffer().put(bytes);
  System.out.println(Arrays.toString(bb.array()));
  for (int i = 1; i <= 3; i++) {
  	  System.out.print(Arrays.toString(Shorts.toByteArray(bb.getShort())) + " ");
  }

输出:

[1, 0, 2, 0, 3, 0]
[0, 1] [0, 2] [0, 3]

翻转了!看来之前的想法是对的!
等等!为啥getShort又给翻回来了???[0, 1] [0, 2] [0, 3] 这是个什么玩意?
懵逼了,撸源码:

static short getShortL(ByteBuffer bb, int bi) {
   return makeShort(bb._get(bi + 1),
                     bb._get(bi    ));
}

妈耶,真的是判断走了getShortL小端处理Short方法,专门把字节翻过来了。
开始思考:
如果

bb.order(ByteOrder.LITTLE_ENDIAN);

在put之前设置,则表示原始数据为小端
注意是为小端,意思是原始数据就是小端模式,ByteBuffer会把原始数据按数据类型翻转为大端存储
比如 0001 0002 0003 short类型,存储结果为 0100 0200 0300
此时,如果getShort,则会再次翻转字节结果为 1 2 3,这样程序才能拿到真正的值!逻辑没毛病!

总结:

使用ByteBuffer处理大小端时,应该为两种情景:

读取原始小端数据

此时应在put之前设置bb.order(ByteOrder.LITTLE_ENDIAN); 表示原始数据为小端模式,保证程序get数据类型时得到正确值

发送数据转换为小端

在put数据之前不应该设置bb.order(ByteOrder.LITTLE_ENDIAN); 意为表示原始数据为大端模式
应在get之前设置 bb.order(ByteOrder.LITTLE_ENDIAN); 此时读取的数据为小端模式,转换为字节数组后即为小端模式

大小端问题始终是以数据类型为基准转换的
例如 0001 00010002(short类型+int类型,大端)
小端模式应为 0100 02000100


版权声明:本文为ZyManTou原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。