已复制
全屏展示
复制代码

Java 之 ByteBuffer 详解


· 7 min read

类ByteBuffer是Java nio程序经常会用到的类,ByteBuffer的核心特性来自Buffer,用于特定基本类型数据的容器。子类ByteBuffer支持除boolean类型以外的全部基本数据类型。Java提供的主要基础数据类型如下

类型 大小 最小值 最大值
byte 8-bit -128 +127
short 16-bit -2^15 +2^15-1
int 32-bit -2^31 +2^31-1
long 64-bit -2^63 +2^63-1
float 32-bit IEEE754 IEEE754
double 64-bit IEEE754 IEEE754
char 16-bit Unicode 0 Unicode 2^16-1

一. ByteBuffer和Buffer核心特性

本质上,Buffer也就是由装有特定基本类型数据的一块内存缓冲区和操作数据的4个指针变量(mark标记,position位置, limit界限,capacity容量)组成。

public abstract class Buffer {  
    private int mark = -1;  
    private int position = 0;  
    private int limit;  
    private int capacity;  
    ......  
}  
  
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>  {  
    final byte[] hb;   // Non-null only for heap buffers  
    final int offset;  
    boolean isReadOnly;   // Valid only for heap buffers  
    ......  
}

其中,字节数组final byte[] hb就是所指的那块内存缓冲区。buffer缓冲区的主要功能特性有:

  • Transferring data  数据传输,主要指可通过get()方法和put()方法向缓冲区存取数据,ByteBuffer提供存取除boolean以为的全部基本类型数据的方法。
  • Marking and resetting  做标记和重置,指mark()方法和reset()方法;而标记,无非是保存操作中某个时刻的索引位置。
  • Invariants 各种指针变量
  • Clearing, flipping, and rewinding  清除数据、位置(position)置0(界限limit为当前位置)、位置(position)置0(界限limit不变),分别指clear()方法, flip()方法和rewind()方法。
  • Read-only buffers 只读缓冲区,指可将缓冲区设为只读。
  • Thread safety 关于线程安全,指该缓冲区不是线程安全的,若多线程操作该缓冲区,则应通过同步来控制对该缓冲区的访问。
  • Invocation chaining 调用链, 指该类的方法返回调用它们的缓冲区,因此,可将方法调用组成一个链;例如:
b.flip();
b.position(23);
b.limit(42);

等同于

b.flip().position(23).limit(42);

二. ByteBuffer组成结构

2.1 四个指针的含义

ByteBuffer主要由是由装数据的内存缓冲区和操作数据的4个指针变量(mark标记,position位置, limit界限,capacity容量)组成。内存缓冲区:字节数组final byte[] hb;

ByteBuffer bb = ByteBuffer.allocate(10);   
bb.put((byte)9);
bb.get();

// bb.put(): 向bb装入byte数据,bb.put((byte)9);执行时,先判断position 是否超过 limit,
// 否则指针 position 向前移一位,将字节 (byte)9 存入 position 所指 byte[] hb 索引位置。

// bb.get(): 获取当前 position 指向的字节
  • position:位置指针。微观上,指向底层字节数组byte[] hb的某个索引位置;宏观上,是ByteBuffer的操作位置,如get()完成后,position指向当前(取出)元素的下一位,put()方法执行完成后,position指向当前(存入)元素的下一位;它是核心位置指针。
  • mark标记:保存某个时刻的position指针的值,通过调用mark()实现;当mark被置为负值时,表示废弃标记。
  • capacity容量:表示 ByteBuffer 的总长度/总容量,也即底层字节数组byte[] hb的容量,一般不可变,用于读取。
  • limit界限:也是位置指针,表示待操作数据的界限,它总是和读取或存入操作相关联,limit指针可以被改变,可以认为limit<=capacity。
指针含义

2.2 ByteBuffer的关键方法实现

  • 取元素
public abstract byte get();  
  
// HeapByteBuffer 子类实现  
public byte get() {  
   return hb[ix(nextGetIndex())];  
}

// HeapByteBuffer 子类方法  
final int nextGetIndex() {                
    if (position >= limit)
       throw new BufferUnderflowException();  
   return position++;  
}
  • 存元素
public abstract ByteBuffer put(byte b);  
  
 // HeapByteBuffer 子类实现  
 public ByteBuffer put(byte x) {  
     hb[ix(nextPutIndex())] = x;  
     return this;  
 }
  • 清除数据
public final Buffer clear() {  
    position = 0;  
    limit = capacity;  
    mark = -1;  
    return this;  
}

可见,对于clear()方法,ByteBuffer只是重置position指针和limit指针,废弃mark标记,并没有真正清空缓冲区/底层字节数组byte[] hb的数据;ByteBuffer也没有提供真正清空缓冲区数据的接口,数据总是被覆盖而不是清空。

对于Socket读操作,从socket中read到数据后,需要从头开始存放到缓冲区,而不是从上次的位置开始继续/连续存放,此时需要使用clear(),重置position指针。需要注意的是,若read到的数据没有填满缓冲区,则socket的read完成后,不能使用array()方法取出缓冲区的数据,因为array()返回的是整个缓冲区的数据,而不是缓冲区的部分数据。

  • 以字节数组形式返回整个缓冲区的数据
public final byte[] array() {
    if (hb == null)
        throw new UnsupportedOperationException();
    if (isReadOnly)
        throw new ReadOnlyBufferException();
    return hb;
}
  • flip-位置重置
public final Buffer flip() {  
    limit = position;  
    position = 0;  
    mark = -1;  
    return this;  
}

socket的read操作完成后,若需要write刚才read到的数据,则需要在write执行前执行flip(),以重置操作位置指针,保存操作数据的界限,保证write数据准确。

  • rewind-位置重置
public final Buffer rewind() {  
     position = 0;  
     mark = -1;  
     return this;  
} 

和flip()相比较而言,没有执行limit = position;

  • 判断剩余的操作数据或者剩余的操作空间
public final int remaining() {  
    return limit - position;  
}

常用于判断socket的write操作中未写出的数据;

  • 标记
public final Buffer mark() {  
    mark = position;  
    return this;  
}
  • 重置到标记
public final Buffer reset() {  
    int m = mark;  
    if (m < 0)
        throw new InvalidMarkException();  
    position = m;  
    return this;  
}  

三. 创建ByteBuffer对象

3.1 allocate方式创建

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}


HeapByteBuffer(int cap, int lim) {            // package-private
    super(-1, 0, lim, cap, new byte[cap], 0);
    /*
    hb = new byte[cap];
    offset = 0;
    */
}


// Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {  // package-private
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}


Buffer(int mark, int pos, int lim, int cap) {       // package-private
    if (cap < 0)
        throw new IllegalArgumentException("Negative capacity: " + cap);
    this.capacity = cap;
    limit(lim);
    position(pos);
    if (mark >= 0) {
        if (mark > pos)
            throw new IllegalArgumentException("mark > position: (" + mark + " > " + pos + ")");
        this.mark = mark;
    }
}

由此可见,allocate方式创建ByteBuffer对象的主要工作包括: 新建底层字节数组byte[] hb(长度为capacity),mark置为-1,position置为0,limit置为capacity,capacity为用户指定的长度。

3.2 wrap方式创建

public static ByteBuffer wrap(byte[] array) {
    return wrap(array, 0, array.length);
}


public static ByteBuffer wrap(byte[] array, int offset, int length) {
    try {
        return new HeapByteBuffer(array, offset, length);
    } catch (IllegalArgumentException x) {
        throw new IndexOutOfBoundsException();
    }
}

HeapByteBuffer(byte[] buf, int off, int len) { // package-private  
    super(-1, off, off + len, buf.length, buf, 0);  
    /* 
    hb = buf; 
    offset = 0; 
    */
}

wrap方式和allocate方式本质相同,不过因为由用户指定的参数不同,参数为byte[] array,所以不需要新建字节数组,byte[] hb置为byte[] array,mark置为-1,position置为0,limit置为array.length,capacity置为array.length。

四. 大顶端与小顶端

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

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

JVM默认为大端模式

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

  • 参考资料

https://blog.csdn.net/skh2015java/article/details/52213334

🔗

文章推荐