NIO源码解析-Buffer简介

前言:

    java.nio包下的Buffer抽象类及其相关实现类,本质上是作为一个固定数量的容器来使用的。

    不同于InputStream和OutputStream时的数据容器byte[],Buffer相关实现类容器可以存储不同基础类型的数据,同时可以对容器中的数据进行检索,反复的操作。

    Buffer(缓冲区)的工作与Channel(通道)紧密相连。Channel是IO发生时的通过的入口(或出口,channel是双向的),而Buffer是这些数据传输的目标(或来源)。

1.Buffer基本属性

    // Invariants: mark <= position <= limit <= capacity
    // 标记地址,与reset搭配使用
    private int mark = -1;
    // 下一个要被读或写的元素的索引
    private int position = 0;
    // 容器现存元素的计数
    private int limit;
    // 容器总容量大小,在Buffer创建时被设定
    private int capacity;

    对于如下一段代码

// position=mark=0
// limit=capacity=10
ByteBuffer buffer = ByteBuffer.allocate(10);

    正是通过以上四个属性,实现了数据的反复操作。

2.Buffer的创建

    在Buffer的实现类中,使用最广泛的还是ByteBuffer,所以以下示例都是基于ByteBuffer(更具体说是HeapByteBuffer)来说明的,后续会有专门的文章来说明其他基本类型的使用及实现。

    根据ByteBuffer的API,我们可以看到以下四种创建方式:

// 1.直接分配capacity大小的Buffer,具体实现类型为HeapByteBuffer
public static ByteBuffer allocate(int capacity)
    
// 2.直接分配capacity大小的Buffer,具体实现类型为DirectByteBuffer
public static ByteBuffer allocateDirect(int capacity)
        
// 3.直接使用array作为底层数据
public static ByteBuffer wrap(byte[] array)
    
// 4.直接使用array作为底层数据,并且指定offset和length
public static ByteBuffer wrap(byte[] array,int offset, int length)

    另:有关于HeapByteBuffer和DirectByteBuffer,笔者会在...中会做详细讲解

2.1 allocate创建方式

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte)'f');
buffer.put((byte)'i');
buffer.put((byte)'r');
buffer.put((byte)'e');

    具体存储图如下:NIO源码解析-Buffer简介

 2.2 wrap创建方式

// wrap(byte[] array,int offset, int length)
// position=offset
// limit=position+length
// capacity=array.length()
ByteBuffer byteBuffer = ByteBuffer.wrap("fire".getBytes(), 1, 2);

    具体存储图如下:

NIO源码解析-Buffer简介

3.Buffer的基本操作方法

3.1 添加数据

ByteBuffer buffer = ByteBuffer.allocate(10);
// 1.逐字节存放 ByteBuffer put(byte b)
buffer.put((byte)'h');

// 2.字节存放到对应index ByteBuffer put(int index, byte b);
buffer.put(0,(byte)'h');

// 3.添加字节数组 ByteBuffer put(byte[] src)
byte[] bytes = {'h','e','l','l','o'};
buffer.put(bytes);

// 4.添加其他基础类型 ByteBuffer putInt(int x) ...
buffer.putInt(1);

3.2 获取数据

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("hello".getBytes());

// 1.获取index下的数据
byte b = buffer.get(0);

// 2.逐个获取(需要先flip下,将position置为0)
buffer.flip();
for (int i = 0; i < buffer.remaining(); i++) {
    byte b1 = buffer.get();
}

// 3.将数据传输到bytes中
buffer.flip();
byte[] bytes = new byte[5];
ByteBuffer byteBuffer = buffer.get(bytes);

3.3 缓冲区翻转

    1)flip是一个比较重要也比较简单的方法,当我们使用put方法将Buffer填充满之后,此时调用get来获取Buffer中的数据时,会获取不到数据,由于get是从当前position来获取数据的,故需要先调用flip来将position置为0

// flip源码如下
// 比较简单,我们也可以手动设置 buffer.limit(buffer.position()).position(0);
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

    2)rewind 相对于flip方法而言,rewind也可以重复读取数据,唯一区别就是没有重新设置limit参数

// 源码如下
public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

    上述两者之间有何不同呢,通过下面一个示例来说明下

ByteBuffer buffer = ByteBuffer.allocate(10);
// 执行完成执行position为4
buffer.put("fire".getBytes());

// 为了测试flip与rewind的不同,重新设置为3
buffer.position(3);

// flip后 pos=0 lim=3 cap=10
buffer.flip();
for (int i = 0; i < buffer.remaining(); i++) {
    byte b1 = buffer.get();
    System.out.println(b1);// 102 105
}

// 注意:需要单独测试,注释掉上述的flip相关代码
// rewind后 pos=0 limit=cap=10
buffer.rewind();
for (int i = 0; i < buffer.remaining(); i++) {
    byte b1 = buffer.get();
    System.out.println(b1);// 102 105 114 101
}

    总结:针对flip而言,flip之后的Buffer数据操作上限就是上次操作到的位置position

    而rewind,上限依旧是limit,可以重新操作全部数据

3.4 缓冲区压缩

    有时我们需要从缓冲区中释放已经操作过的数据,然后重新填充数据(针对未操作过的数据,我们是需要保留的)。

    我们可以将未操作过的数据(也就是position-limit之间的数据),重新拷贝到0位置,即可实现上述需求。而Buffer中已经针对这种场景实现了具体方法,也就是compact方法

// 示例如下
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("fire".getBytes());

// 获取一个数据,position前移
buffer.flip();
buffer.get();

// 进行数据压缩
ByteBuffer compact = buffer.compact();

    压缩前的buffer:NIO源码解析-Buffer简介

     压缩后的buffer:NIO源码解析-Buffer简介

    相比较而言:将原position到limit之间的数据(1-4,也就是i r e)拷贝到index=0位置,position也就是3,后续新写入数据直接覆盖原position=3的位置数据。

    

3.5 标记与重置

    mark和reset方法,mark用来做标记,reset用来调回到做标记的位置。比较简单,直接看示例

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("fire".getBytes());

// 直接将position置为2
buffer.position(2);

// 做标记,在position=2位置做标记
buffer.mark();

// 获取position=2的数据,
byte b = buffer.get(); // 114
// 获取下一个position的数据后,执行reset,然后重新获取数据,发现是同一个数据
buffer.reset();
byte c = buffer.get(); // 114

    经过reset操作后,position重新回到2,也就是mark时的position,故两次get方法获取的是同一个position的值

3.6 复制

    Buffer还提供了快速复制一个Buffer的功能

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("fire".getBytes());

// 复制buffer
ByteBuffer duplicate = buffer.duplicate();

    复制后的duplicate与buffer共享源数据数组,只是拥有不同的position、limitNIO源码解析-Buffer简介

总结:

    Buffer作为数据存储的容器,其有很多的实现类和API,本文中对其基本API进行了分析,后续我们继续对其实现类进行分析。

上一篇:【总结】JavaIO


下一篇:Java-IO流系列-NIO概述