Java
语言基础
NIO

Java NIO(一):Buffer

简介:Buffer在Java NIO中主要负责数据的存取,缓冲区就是数组,用于存储不同数据类型的数据。

1. Buffer

Buffer在Java NIO中主要负责数据的存取,缓冲区就是数组,用于存储不同数据类型的数据。根据数据类型不同,提供了相应类型的缓冲区(除了boolean之外),即 Buffer 的子类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer和ShortBuffer。它们的类图结构如下:

1.Buffer结构图.png

这些缓冲区类都是抽象类,而且它们的管理方式几乎一致,都是通过allocate()来获取具体的实现类(通常都是对应Buffer的子类)。我们可以把Buffer理解为一个数组,IntBuffer、CharBuffer、DoubleBuffer 等分别对应int[]char[]double[]等。其中最重要的是ByteBuffer类,其他的类几乎都是通过包装ByteBuffer实现的。本篇文章也主要以ByteBuffer为例来介绍Buffer的。

2. 几个重要变量

在Buffer中,存在几个重要的变量,很多人在学习Buffer的时候都不能很好的理解这几个变量的作用。我们稍后会以一个连串的例子来解释这几个变量,这里先对他们做基本概念的阐述:

  • capacitycapacity主要用于标识Buffer的总大小,一旦设定就不可以更改,例如当创建ByteBuffer时指定其大小为8,那么ByteBuffer底层会维护一个byte[8]的数组。
  • limitlimit用于标识可操作数据的范围。在写操作模式下,limit代表的是最大能写入的数据,这个时候limit等于capacity。写结束后,切换到读模式,此时的limit等于Buffer中实际的数据大小,因为Buffer不一定被写满了。
  • positionposition用于标识当前操作的位置。

3. 分步操作案例

对缓冲区的操作无非就是读写两种,Buffer只是在读写的基础上扩展了一些便捷方法,便于我们更好的操作缓冲区。在本案例中以一个ByteBuffer为例,详细演示其各类操作。需要注意的是,下面的例子是顺序执行的,每一节都是以上一节的结果继续执行。

3.1. 初始化ByteBuffer

要使用ByteBuffer,必然先要初始化,前面提到过,所有的缓冲区类都是通过allocate()方法来管理的,因此想要初始化一个ByteBuffer实例,使用该方法即可。在初始化时我们需要制定缓冲区大小,这个大小一旦指定就不可更改,这里我们指定为8。同时需要注意的是,缓冲区在初始化之后都处于写模式。具体代码如下:

  • System.out.println("===============> allocate ");
  • // 分配一个指定大小的缓冲区
  • ByteBuffer byteBuffer = ByteBuffer.allocate(8);
  • System.out.println("after allocate, position: " + byteBuffer.position()); // 0
  • System.out.println("after allocate, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after allocate, capacity: " + byteBuffer.capacity()); // 8

在上面的代码中,使用ByteBuffer.allocate(8)创建了一个大小为8的ByteBuffer,同时打印了创建后的position、limit和capacity值,运行结果打印如下:

  • ===============> allocate
  • after allocate, position: 0
  • after allocate, limit: 8
  • after allocate, capacity: 8

从打印结果可以得知,初始化一个大小为8的ByteBuffer后,capacity和limit都为8,而position为0。这很好理解,根据我们之前对这三个属性的解释,表示此时缓冲区容量为8(8即为capacity),可写范围为[0, 8)(8即为limit的值),当前操作位置为0(0即为position的值)。这三个属性的存储示意图如下:

2.allocate.png

注:索引为8的位置并不存储数据。

我们可以跟踪allocate()方法的源码:

  • public static ByteBuffer allocate(int capacity) {
  • if (capacity < 0)
  • throw new IllegalArgumentException();
  • return new HeapByteBuffer(capacity, capacity);
  • }
  • // package-private
  • HeapByteBuffer(int cap, int lim) {
  • super(-1, 0, lim, cap, new byte[cap], 0);
  • /*
  • hb = new byte[cap];
  • offset = 0;
  • */
  • }
  • // package-private
  • ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
  • super(mark, pos, lim, cap);
  • this.hb = hb;
  • this.offset = offset;
  • }

会发现该方法底层调用了HeapByteBuffer类的构造方法,传入的参数即是:postition为0,limit为capacity,同时mark为-1。

3.1.1. 直接字节缓冲区

还可以使用allocateDirect()方法分配直接内存。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区,示例代码:

  • // 分配1G直接内存
  • ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(16 * 1024 * 1024);
  • System.out.println("directByteBuffer capacity: " + directByteBuffer.capacity()); // 16777216

直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机I/O操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。

如果分配直接字节缓冲区,Java虚拟机会尽最大努力直接在此缓冲区上执行本机I/O操作。也就是说,在每次调用基础操作系统的一个本机I/O操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。

直接字节缓冲区还可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer。Java平台的实现有助于通过JNI从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。

字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理。

3.2. 向ByteBuffer存入数据

ByteBuffer初始化后,就默认处于写模式,这时可以使用put()方法向其中写入数据,本例中我们向其中写入字符串“hello”:

  • // 写数据模式
  • System.out.println("===============> put ");
  • byteBuffer.put("hello".getBytes());
  • System.out.println("after put, position: " + byteBuffer.position()); // 5
  • System.out.println("after put, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after put, capacity: " + byteBuffer.capacity()); // 8

运行之后打印结果如下:

  • ===============> put
  • after put, position: 5
  • after put, limit: 8
  • after put, capacity: 8

可以发现存入数据之后position从0调整为5,limit和capacity都没有改变。写入操作之后后的存储示意图如下:

3.put.png

在ByteBuffer的写入操作中,每次写入一个字节的数据,position都会往后移动一位,因此在写入“hello”字符串后,position会移动至索引为5的位置。依旧是跟踪写入操作的源码:

  • // ByteBuffer类中
  • public final ByteBuffer put(byte[] src) {
  • return put(src, 0, src.length);
  • }
  • // ByteBuffer类中
  • public ByteBuffer put(byte[] src, int offset, int length) {
  • checkBounds(offset, length, src.length);
  • if (length > remaining())
  • throw new BufferOverflowException();
  • int end = offset + length;
  • for (int i = offset; i < end; i++)
  • this.put(src[i]);
  • return this;
  • }
  • // Buffer类中
  • static void checkBounds(int off, int len, int size) { // package-private
  • if ((off | len | (off + len) | (size - (off + len))) < 0)
  • throw new IndexOutOfBoundsException();
  • }
  • // HeapByteBuffer类中
  • public ByteBuffer put(byte x) {
  • hb[ix(nextPutIndex())] = x;
  • return this;
  • }
  • // HeapByteBuffer类中
  • final int nextPutIndex() { // package-private
  • if (position >= limit)
  • throw new BufferOverflowException();
  • return position++;
  • }
  • // HeapByteBuffer类中。此处的offset即为allocate初始化时使用到的offset,在本例中为1
  • protected int ix(int i) {
  • return i + offset;
  • }

从源码可以得知,在写入操作时调用的是重载的put方法,在这个put方法中会首先检查索引是否正确,如果不正确会抛出IndexOutOfBoundsException异常;然后检查ByteBuffer空闲空间是否满足写入数据的长度,如果不满足将抛出BufferOverflowException异常。一切检查通过后会循环将传入的byte数组中的数据依次写入ByteBuffer内部维护的数据数组中从position位置开始的空间内,每次写入都将position加1,以便循环的下一次的写入。

3.3. 切换为读模式

在向其中添加数据之后,如果想要读取添加的数据,就首先需要将ByteBuffer从写模式转换为读模式,需要调用flip()操作:

  • // 转换写模式为读模式
  • System.out.println("===============> flip ");
  • byteBuffer.flip();
  • System.out.println("after flip, position: " + byteBuffer.position()); // 0
  • System.out.println("after flip, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after flip, capacity: " + byteBuffer.capacity()); // 8

运行结果如下:

  • ===============> flip
  • after flip, position: 0
  • after flip, limit: 5
  • after flip, capacity: 8

不明白flip操作做了什么?我们可以观察flip()方法的源码:

  • public final Buffer flip() {
  • limit = position;
  • position = 0;
  • mark = -1;
  • return this;
  • }

flip()的作用非常简单,将limit设置为position的值(即从8调整为5),将position设置为0(即从5调整为0),将mark设置为-1。完成flip操作之后的示意图如下:

4.filp.png

flip()方法之所以能将写模式转换为读模式,是因为此时position和limit都改变了,还记得最开始对position和limit的解释吗?position用于标识当前操作的位置,limit用于标识可操作数据的范围。切换到读模式后的limit等于Buffer中实际存储的数据的大小。因此,此时的[position, limit)范围内正是我们之前写入的数据。

3.4. 读取数据

在使用flip()切换为读模式之后,就能够进行数据读取了。需要准备一个字节数组用于装载读取的数据,我们先读取3个字节的数据:

  • // 读取缓冲区数据
  • System.out.println("===============> get ");
  • byte[] bytes = new byte[3];
  • byteBuffer.get(bytes);
  • System.out.println("get result: " + new String(bytes, 0, bytes.length));
  • System.out.println("after get, position: " + byteBuffer.position()); // 3
  • System.out.println("after get, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after get, capacity: " + byteBuffer.capacity()); // 8

运行结果如下:

  • ===============> get
  • get result: hel
  • after get, position: 3
  • after get, limit: 5
  • after get, capacity: 8

读取的数据全部存储在新创建的bytes数组中,同时读取完后仅仅只有position的值从0调整为3(3即为bytes.length),读取完后示意图如下:

5.get.png

图中绿色的区域代表已经读取到的数据,此时position刚好在绿色区域的后面一位的索引位置上。跟踪读取操作的源码:

  • // ByteBuffer类中
  • public ByteBuffer get(byte[] dst) {
  • return get(dst, 0, dst.length);
  • }
  • // ByteBuffer类中
  • public ByteBuffer get(byte[] dst, int offset, int length) {
  • checkBounds(offset, length, dst.length);
  • if (length > remaining())
  • throw new BufferUnderflowException();
  • int end = offset + length;
  • for (int i = offset; i < end; i++)
  • dst[i] = get();
  • return this;
  • }
  • // Buffer类中
  • static void checkBounds(int off, int len, int size) { // package-private
  • if ((off | len | (off + len) | (size - (off + len))) < 0)
  • throw new IndexOutOfBoundsException();
  • }
  • // HeapByteBuffer类中
  • public byte get() {
  • return hb[ix(nextGetIndex())];
  • }
  • // Buffer类中
  • final int nextGetIndex() { // package-private
  • if (position >= limit)
  • throw new BufferUnderflowException();
  • return position++;
  • }
  • // HeapByteBuffer类中。此处的offset即为allocate初始化时使用到的offset,在本例中为1
  • protected int ix(int i) {
  • return i + offset;
  • }

读取操作与写入操作非常相似,它调用的是重载的get方法,在这个get方法中会首先检查索引是否正确,如果有误会抛出IndexOutOfBoundsException异常;然后检查ByteBuffer内的数据是否满足写入数据的长度,如果不满足将抛出BufferUnderflowException异常。一切检查通过后会循环从ByteBuffer内部维护的数据数组中自position位置开始依次取出数据装载到传入的bytes数组中,每次读取都将position加1,以便循环的下一次的读取。

从源码解析可知,如果多次调用get读取,将会是顺序读取ByteBuffer中的数据(每次都是从position位置开始读取,而每次读取后position值都会更新)。

3.5. 重复读取

从上面的get读取操作可知,如果多次调用get操作,将会是顺序读取ByteBuffer中的数据,position会随着读取的数据逐渐递增,直至等于limit(大于limit会抛出BufferUnderflowException异常)。如果我们想要对ByteBuffer中的数据从头重复读取,就需要用到rewind操作,先看示例代码:

  • // 可重复读数据
  • System.out.println("===============> rewind ");
  • byteBuffer.rewind();
  • System.out.println("after rewind, position: " + byteBuffer.position()); // 0
  • System.out.println("after rewind, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after rewind, capacity: " + byteBuffer.capacity()); // 8
  • byteBuffer.get(bytes);
  • System.out.println("get result: " + new String(bytes, 0, bytes.length));

运行的结果如下:

  • ===============> rewind
  • after rewind, position: 0
  • after rewind, limit: 5
  • after rewind, capacity: 8
  • get result: hel

rewind()方法的操作之后,position的值从3调整为0,而其他属性都没改变。这时我们就可以重复进行读取了。rewind之后示意图如下:

6.rewind.png

查看rewind的源码,非常简单:

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

在rewind操作之后,我们又使用之前创建的bytes数组进行了一次读取,bytes数组的长度为3,所以会读取到hel三个字符,读取操作结束后,存储示意图如下:

7.get.png

3.6. 添加标记和重置position

rewind的操作是让position恢复为0,然后可以从头重复读取。如果我们想要记录某个位置,下次读取时从这个位置开始重复读取,就需要用到mark和reset操作的配合了,先看mark操作的代码:

  • // mark可以记录当前position的位置,然后可以通过reset()恢复到mark的位置
  • System.out.println("===============> mark ");
  • // 记录position的位置
  • byteBuffer.mark();
  • System.out.println("position when mark: " + byteBuffer.position()); // position when mark: 3
  • System.out.println("after mark, position: " + byteBuffer.position()); // 3
  • System.out.println("after mark, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after mark, capacity: " + byteBuffer.capacity()); // 8

运行结果如下:

  • ===============> mark
  • position when mark: 3
  • after mark, position: 3
  • after mark, limit: 5
  • after mark, capacity: 8

mark操作会将ByteBuffer内部维护的mark属性(该属性对外部不可见)设置为当前的position值,源码如下:

  • public final Buffer mark() {
  • mark = position;
  • return this;
  • }

mark操作之后,存储示意图如下:

8.mark.png

可以发现,由于执行mark操作时position值为3,所以mark操作之后,mark值将和position值相同。进行mark操作之后,我们接着读取两个字节的数据:

  • byte[] bytes1 = new byte[2];
  • byteBuffer.get(bytes1);
  • System.out.println("get result: " + new String(bytes1, 0, bytes1.length)); // lo

运行结果打印如下:

  • get result: lo

在这个读取操作执行完后,存储示意图变成了:

9.get.png

可以发现,此时position因为读取数据操作已经从3调整为5,而mark的值还没有变化,因此,mark的值记录了读操作之前position的值。

如果我们想要将position恢复为mark记录的值,然后重新读取上面的数据,只需要调用reset操作即可,示例代码如下:

  • System.out.println("===============> reset ");
  • for (int i = 0; i < 3; i++) {
  • System.out.println("times " + i + ", position before reset: " + byteBuffer.position()); // before reset: 5
  • // 还原position为之前mark时记录的位置
  • byteBuffer.reset();
  • System.out.println("times " + i + ", position after reset: " + byteBuffer.position()); // after reset: 3
  • byteBuffer.get(bytes1);
  • System.out.println("times " + i + ", get result: " + new String(bytes1, 0, bytes1.length)); // lo
  • }

在上面的代码的循环体中,每次都先调用reset操作,然后进行数据的读取,一共循环了三次,运行的结果如下:

  • ===============> reset
  • times 0, position before reset: 5
  • times 0, position after reset: 3
  • times 0, get result: lo
  • times 1, position before reset: 5
  • times 1, position after reset: 3
  • times 1, get result: lo
  • times 2, position before reset: 5
  • times 2, position after reset: 3
  • times 2, get result: lo

可以发现,每次执行reset操作之前,position的值为5,在执行reset操作之后position将会更新为mark属性记录的值,即3,然后进行读取操作。reset操作的源码也非常简单,它会首先检查mark属性的值是否大于等于0,如果不满足将抛出InvalidMarkException异常,如果满足会将position的值设置为mark属性的值:

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

reset操作之后存储示意图如下:

10.reset.png

reset之后的get操作后存储示意图如下:

11.get.png

3.7. 压缩操作

ByteBuffer提供了compact()方法用于压缩缓冲区。这里压缩的意思是:保留未读取数据,舍弃已读取数据。它的具体操作是将[position, limit)范围的数据复制到[0, limit - position - 1]的位置,然后将position置为limit - position的值。前面的叙述太抽象,直白点说,其实[position, limit)范围内的就是没有读取的数据,把它复制出来,然后写入到从索引0位置开始的位置即可(其实源码也是这么实现的)。我们依旧是看示例代码:

  • // 压缩操作
  • System.out.println("===============> compact ");
  • byteBuffer.reset();
  • System.out.println("after reset, position: " + byteBuffer.position()); // 3
  • System.out.println("after reset, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after reset, capacity: " + byteBuffer.capacity()); // 8
  • byteBuffer.compact();
  • System.out.println("after compact, position: " + byteBuffer.position()); // 2
  • System.out.println("after compact, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after compact, capacity: " + byteBuffer.capacity()); // 8
  • byteBuffer.flip();
  • System.out.println("after flip, position: " + byteBuffer.position()); // 0
  • System.out.println("after flip, limit: " + byteBuffer.limit()); // 2
  • System.out.println("after flip, capacity: " + byteBuffer.capacity()); // 8
  • byte[] bytes2 = new byte[byteBuffer.limit()];
  • byteBuffer.get(bytes2);
  • System.out.println("get result: " + new String(bytes2, 0, bytes2.length)); // ll

上述的代码运行结果如下:

  • ===============> compact
  • after reset, position: 3
  • after reset, limit: 5
  • after reset, capacity: 8
  • after compact, position: 2
  • after compact, limit: 8
  • after compact, capacity: 8
  • after flip, position: 0
  • after flip, limit: 2
  • after flip, capacity: 8
  • get result: lo

这里对上面的运行结果做详细解释。由于之前进行过mark操作,同时测试时进行了读取,所以一开始先进行reset复位position为mark属性记录的值,此时存储示意图如下:

12.reset.png

此时position位于索引为3的位置,因此,未读取的数据在[3, 5)的范围内,也就是lo两个字符,调用compact操作后,会将这两个字符复制到ByteBuffer的头部位置,compact后存储示意图如下:

13.compact.png

你一定会发现,此时其实position的值已经调整为2了,且limit的值调整成和capacity一样了,也就是说,comapct操作会直接转换为写模式,同时,compact操作也是一种清空操作,它清空了已读取的数据(但没有被覆盖的数据还是存在于ByteBuffer中的)。接下来我们使用flip操作转换为读模式,操作后的存储示意图如下:

14.flip.png

flip操作之后直接进行读取,会读取到compact操作转存到开头的lo两个字符。读取完后存储示意图如下:

15.get.png

以上就是compact操作的作用,当我们想要在保存未读取数据的情况下清空缓冲区,compact操作就是为这种情况设计的。compact()方法的源码也非常简单:

  • // HeapByteBuffer类中
  • public ByteBuffer compact() {
  • // 首先进行copy操作,将未读取数据拷贝到缓冲区开头的位置
  • System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
  • // 设置position的值为未读取数据的数量
  • position(remaining());
  • // 将limit设置为capacity
  • limit(capacity());
  • // 将mark设置为-1
  • discardMark();
  • return this;
  • }
  • // HeapByteBuffer类中
  • protected int ix(int i) {
  • return i + offset;
  • }
  • // Buffer类中
  • public final int position() {
  • return position;
  • }
  • // Buffer类中
  • public final int remaining() {
  • return limit - position;
  • }
  • // Buffer类中
  • public final Buffer position(int newPosition) {
  • if ((newPosition > limit) || (newPosition < 0))
  • throw new IllegalArgumentException();
  • position = newPosition;
  • if (mark > position) mark = -1;
  • return this;
  • }
  • // Buffer类中
  • public final int capacity() {
  • return capacity;
  • }
  • // Buffer类中
  • public final Buffer limit(int newLimit) {
  • if ((newLimit > capacity) || (newLimit < 0))
  • throw new IllegalArgumentException();
  • limit = newLimit;
  • if (position > limit) position = limit;
  • if (mark > limit) mark = -1;
  • return this;
  • }
  • // Buffer类中
  • final void discardMark() { // package-private
  • mark = -1;
  • }

3.8. 缓冲区副本

ByteBuffer还提供了两个不是很常用的方法:duplicate()slice()。这两个方法都用于创建缓冲区副本,唯一不同的是,duplicate()是以原缓冲区所有的数据来创建副本的,而slice()则是以原缓冲区的position位置开始的数据创建副本。

虽说是副本,但其实副本缓冲区的数据和原缓冲区的数据是共享的,也即是说,修改原缓冲区的数据时,副本缓冲区的数据也会发生变化,副本缓冲区与原缓冲区的position、limit及mark是相互依赖的。

  • 对于slice()方法创建的副本缓冲区而言,它的position为0,limit和capacity为原缓冲区剩余数据的字节数,mark值是未定义的(即-1);
  • 对于duplicate()方法创建的副本缓冲区而言,它的position、limit、capacity和mark与原缓冲区完全相同;
  • 对于这两个方法创建的副本缓冲区而言,如果原缓冲区是直接内存缓冲区,那么副本缓冲区也会是直接内存缓冲区;如果原缓冲区是只读的,那么副本缓冲区也会是只读的。
  • // 副本操作
  • System.out.println("===============> duplicate ");
  • ByteBuffer duplicateByteBuffer = byteBuffer.duplicate();
  • System.out.println("duplicateByteBuffer, position: " + duplicateByteBuffer.position()); // 2
  • System.out.println("duplicateByteBuffer, limit: " + duplicateByteBuffer.limit()); // 2
  • System.out.println("duplicateByteBuffer, capacity: " + duplicateByteBuffer.capacity()); // 8
  • System.out.println("===============> duplicate rewind");
  • duplicateByteBuffer.rewind();
  • System.out.println("duplicateByteBuffer, position: " + duplicateByteBuffer.position()); // 0
  • System.out.println("duplicateByteBuffer, limit: " + duplicateByteBuffer.limit()); // 2
  • System.out.println("duplicateByteBuffer, capacity: " + duplicateByteBuffer.capacity()); // 8
  • System.out.println("byteBuffer, position: " + byteBuffer.position()); // 2
  • System.out.println("byteBuffer, limit: " + byteBuffer.limit()); // 2
  • System.out.println("byteBuffer, capacity: " + byteBuffer.capacity()); // 8
  • System.out.println("===============> duplicate modified");
  • duplicateByteBuffer.put("A".getBytes());
  • System.out.println("duplicateByteBuffer String: " + new String(duplicateByteBuffer.array(), 0, duplicateByteBuffer.limit())); // Ao
  • System.out.println("byteBuffer String: " + new String(byteBuffer.array(), 0, byteBuffer.limit())); // Ao
  • System.out.println("===============> slice ");
  • ByteBuffer sliceByteBuffer = byteBuffer.slice();
  • System.out.println("sliceByteBuffer, position: " + sliceByteBuffer.position()); // 0
  • System.out.println("sliceByteBuffer, limit: " + sliceByteBuffer.limit()); // 0
  • System.out.println("sliceByteBuffer, capacity: " + sliceByteBuffer.capacity()); // 8
  • System.out.println("===============> slice rewind");
  • sliceByteBuffer.rewind();
  • System.out.println("sliceByteBuffer, position: " + sliceByteBuffer.position()); // 0
  • System.out.println("sliceByteBuffer, limit: " + sliceByteBuffer.limit()); // 0
  • System.out.println("sliceByteBuffer, capacity: " + sliceByteBuffer.capacity()); // 8
  • System.out.println("byteBuffer, position: " + byteBuffer.position()); // 2
  • System.out.println("byteBuffer, limit: " + byteBuffer.limit()); // 2
  • System.out.println("byteBuffer, capacity: " + byteBuffer.capacity()); // 8

上述的代码运行结果如下:

  • ===============> duplicate
  • duplicateByteBuffer, position: 2
  • duplicateByteBuffer, limit: 2
  • duplicateByteBuffer, capacity: 8
  • ===============> duplicate rewind
  • duplicateByteBuffer, position: 0
  • duplicateByteBuffer, limit: 2
  • duplicateByteBuffer, capacity: 8
  • byteBuffer, position: 2
  • byteBuffer, limit: 2
  • byteBuffer, capacity: 8
  • ===============> duplicate modified
  • duplicateByteBuffer String: Ao
  • byteBuffer String: Ao
  • ===============> slice
  • sliceByteBuffer, position: 0
  • sliceByteBuffer, limit: 0
  • sliceByteBuffer, capacity: 0
  • ===============> slice rewind
  • sliceByteBuffer, position: 0
  • sliceByteBuffer, limit: 0
  • sliceByteBuffer, capacity: 0
  • byteBuffer, position: 2
  • byteBuffer, limit: 2
  • byteBuffer, capacity: 8

在compact操作之后,我们读完了原缓冲区中的数据,此时position和limit都为2,此时如果对原缓冲区执行duplicate操作时,会原封不动地使用原缓冲区的position、limit、mark和capacity,因此duplicateByteBuffer的position、limit和capacity分别为2、2、8;duplicate操作的示意图如下:

16.duplicate.png

而slice操作则不一样了,因为原缓冲区数据已被读完,因此slice操作得到的副本缓冲区中将没有可读数据,即剩余可读字节数为0.因此duplicateByteBuffer的position、limit和capacity都为0;slice操作的示意图如下:

17.slice.png

在使用两个方法创建副本缓冲区后,都使用rewind操作进行了测试,可以发现两个副本缓冲区的position虽然发生了改变,但原缓冲区的position并没有改变在,这也印证了原缓冲区和副本缓冲区的position是相互独立的,limit及mark也是一样,读者可以自行测试。

同时,在改变duplicate的副本缓冲区的数据时,原缓冲区的数据也发生了改变,这一点印证了副本缓冲区的数据和原缓冲区的数据是共享的。

3.9. 判断是否有未读数据

hasRemaining()方法可以用于判断ByteBuffer中是否还存在未读取的数据,它的源码如下:

  • public final boolean hasRemaining() {
  • return position < limit;
  • }

即判断position是否小于limit。示例代码如下:

  • // 查看缓冲区中是否还有数据
  • System.out.println("===============> hasRemaining ");
  • System.out.println("hasRemaining: " + byteBuffer.hasRemaining()); // false

运行结果将打印:

  • ===============> hasRemaining
  • hasRemaining: false

3.10. 清空缓冲区

当我们使用完缓冲区之后,想要将其清空以便重新使用,可以调用clear()方法,示例代码如下:

  • // 清空缓冲区
  • System.out.println("===============> clear ");
  • byteBuffer.clear();
  • System.out.println("after clear, position: " + byteBuffer.position()); // 0
  • System.out.println("after clear, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after clear, capacity: " + byteBuffer.capacity()); // 8
  • // 虽然清空了缓冲区,但缓冲区中的数据还存在,这些数据处于“被遗忘”状态
  • for (int i = 0; i < byteBuffer.limit(); i++) {
  • System.out.println("Garbage datas: " + (char) byteBuffer.get(i));
  • }

需要注意的是,虽然clear()方法可用作清空缓冲区,但它的实现仅仅是:

  • public final Buffer clear() {
  • position = 0;
  • limit = capacity;
  • mark = -1;
  • return this;
  • }

即将position置为0,limit置为与capacity一样,mark置为-1。因此其实缓冲区中的数据还是存在的,这些数据将处于“被遗忘”状态。我们可以推断clear操作之后存储示意图如下:

18.clear.png

在上面的实例代码中我们仍然尝试将其读出来了,运行结果如下:

  • ===============> clear
  • after clear, position: 0
  • after clear, limit: 8
  • after clear, capacity: 8
  • Garbage datas: A
  • Garbage datas: o
  • Garbage datas: l
  • Garbage datas: l
  • Garbage datas: o
  • Garbage datas: x // 这里其实读出来是乱码,无法显示,所以用x表示
  • Garbage datas: x // 这里其实读出来是乱码,无法显示,所以用x表示
  • Garbage datas: x // 这里其实读出来是乱码,无法显示,所以用x表示

注:其中的x其实是乱码数据,因无法显示才写为x

4. 案例完整代码和流程示意图

这里贴上上述案例的完整代码:

  • package com.coderap;
  • import java.nio.ByteBuffer;
  • /**
  • * @program: Java-Practices
  • * @description:
  • * @author: Lennon Chin
  • * @create: 2018/08/22 20:57:38
  • */
  • public class ByteBufferTest {
  • public static void main(String[] args) {
  • // 分配一个指定大小的缓冲区
  • System.out.println("===============> allocate ");
  • ByteBuffer byteBuffer = ByteBuffer.allocate(8);
  • System.out.println("after allocate, position: " + byteBuffer.position()); // 0
  • System.out.println("after allocate, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after allocate, capacity: " + byteBuffer.capacity()); // 8
  • // 写数据模式
  • System.out.println("===============> put ");
  • byteBuffer.put("hello".getBytes());
  • System.out.println("after put, position: " + byteBuffer.position()); // 5
  • System.out.println("after put, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after put, capacity: " + byteBuffer.capacity()); // 8
  • // 转换写模式为读模式
  • System.out.println("===============> flip ");
  • byteBuffer.flip();
  • System.out.println("after flip, position: " + byteBuffer.position()); // 0
  • System.out.println("after flip, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after flip, capacity: " + byteBuffer.capacity()); // 8
  • // 读取缓冲区数据
  • System.out.println("===============> get ");
  • byte[] bytes = new byte[3];
  • byteBuffer.get(bytes);
  • System.out.println("get result: " + new String(bytes, 0, bytes.length));
  • System.out.println("after get, position: " + byteBuffer.position()); // 3
  • System.out.println("after get, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after get, capacity: " + byteBuffer.capacity()); // 8
  • // 可重复读数据
  • System.out.println("===============> rewind ");
  • byteBuffer.rewind();
  • System.out.println("after rewind, position: " + byteBuffer.position()); // 0
  • System.out.println("after rewind, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after rewind, capacity: " + byteBuffer.capacity()); // 8
  • byteBuffer.get(bytes);
  • System.out.println("get result: " + new String(bytes, 0, bytes.length));
  • // mark可以记录当前position的位置,然后可以通过reset()恢复到mark的位置
  • System.out.println("===============> mark ");
  • // 记录position的位置
  • byteBuffer.mark();
  • System.out.println("position when mark: " + byteBuffer.position()); // position when mark: 3
  • System.out.println("after mark, position: " + byteBuffer.position()); // 3
  • System.out.println("after mark, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after mark, capacity: " + byteBuffer.capacity()); // 8
  • byte[] bytes1 = new byte[2];
  • byteBuffer.get(bytes1);
  • System.out.println("get result: " + new String(bytes1, 0, bytes1.length)); // lo
  • System.out.println("===============> reset ");
  • for (int i = 0; i < 3; i++) {
  • System.out.println("times " + i + ", position before reset: " + byteBuffer.position()); // before reset: 5
  • // 还原position为之前mark时记录的位置
  • byteBuffer.reset();
  • System.out.println("times " + i + ", position after reset: " + byteBuffer.position()); // after reset: 3
  • byteBuffer.get(bytes1);
  • System.out.println("times " + i + ", get result: " + new String(bytes1, 0, bytes1.length)); // lo
  • }
  • // 压缩操作
  • System.out.println("===============> compact ");
  • byteBuffer.reset();
  • System.out.println("after reset, position: " + byteBuffer.position()); // 3
  • System.out.println("after reset, limit: " + byteBuffer.limit()); // 5
  • System.out.println("after reset, capacity: " + byteBuffer.capacity()); // 8
  • byteBuffer.compact();
  • System.out.println("after compact, position: " + byteBuffer.position()); // 2
  • System.out.println("after compact, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after compact, capacity: " + byteBuffer.capacity()); // 8
  • byteBuffer.flip();
  • System.out.println("after flip, position: " + byteBuffer.position()); // 0
  • System.out.println("after flip, limit: " + byteBuffer.limit()); // 2
  • System.out.println("after flip, capacity: " + byteBuffer.capacity()); // 8
  • byte[] bytes2 = new byte[byteBuffer.limit()];
  • byteBuffer.get(bytes2);
  • System.out.println("get result: " + new String(bytes2, 0, bytes2.length)); // ll
  • // 副本操作
  • System.out.println("===============> duplicate ");
  • ByteBuffer duplicateByteBuffer = byteBuffer.duplicate();
  • System.out.println("duplicateByteBuffer, position: " + duplicateByteBuffer.position()); // 2
  • System.out.println("duplicateByteBuffer, limit: " + duplicateByteBuffer.limit()); // 2
  • System.out.println("duplicateByteBuffer, capacity: " + duplicateByteBuffer.capacity()); // 8
  • System.out.println("===============> duplicate rewind");
  • duplicateByteBuffer.rewind();
  • System.out.println("duplicateByteBuffer, position: " + duplicateByteBuffer.position()); // 0
  • System.out.println("duplicateByteBuffer, limit: " + duplicateByteBuffer.limit()); // 2
  • System.out.println("duplicateByteBuffer, capacity: " + duplicateByteBuffer.capacity()); // 8
  • System.out.println("byteBuffer, position: " + byteBuffer.position()); // 2
  • System.out.println("byteBuffer, limit: " + byteBuffer.limit()); // 2
  • System.out.println("byteBuffer, capacity: " + byteBuffer.capacity()); // 8
  • System.out.println("===============> duplicate modified");
  • duplicateByteBuffer.put("A".getBytes());
  • System.out.println("duplicateByteBuffer String: " + new String(duplicateByteBuffer.array(), 0, duplicateByteBuffer.limit())); // Ao
  • System.out.println("byteBuffer String: " + new String(byteBuffer.array(), 0, byteBuffer.limit())); // Ao
  • System.out.println("===============> slice ");
  • ByteBuffer sliceByteBuffer = byteBuffer.slice();
  • System.out.println("sliceByteBuffer, position: " + sliceByteBuffer.position()); // 0
  • System.out.println("sliceByteBuffer, limit: " + sliceByteBuffer.limit()); // 0
  • System.out.println("sliceByteBuffer, capacity: " + sliceByteBuffer.capacity()); // 8
  • System.out.println("===============> slice rewind");
  • sliceByteBuffer.rewind();
  • System.out.println("sliceByteBuffer, position: " + sliceByteBuffer.position()); // 0
  • System.out.println("sliceByteBuffer, limit: " + sliceByteBuffer.limit()); // 0
  • System.out.println("sliceByteBuffer, capacity: " + sliceByteBuffer.capacity()); // 8
  • System.out.println("byteBuffer, position: " + byteBuffer.position()); // 2
  • System.out.println("byteBuffer, limit: " + byteBuffer.limit()); // 2
  • System.out.println("byteBuffer, capacity: " + byteBuffer.capacity()); // 8
  • // 查看缓冲区中是否还有数据
  • System.out.println("===============> hasRemaining ");
  • System.out.println("hasRemaining: " + byteBuffer.hasRemaining()); // false
  • // 清空缓冲区
  • System.out.println("===============> clear ");
  • byteBuffer.clear();
  • System.out.println("after clear, position: " + byteBuffer.position()); // 0
  • System.out.println("after clear, limit: " + byteBuffer.limit()); // 8
  • System.out.println("after clear, capacity: " + byteBuffer.capacity()); // 8
  • // 虽然清空了缓冲区,但缓冲区中的数据还存在,这些数据处于“被遗忘”状态
  • for (int i = 0; i < byteBuffer.limit(); i++) {
  • System.out.println("Garbage datas: " + (char) byteBuffer.get(i));
  • }
  • }
  • }

运行结果:

  • ===============> allocate
  • after allocate, position: 0
  • after allocate, limit: 8
  • after allocate, capacity: 8
  • ===============> put
  • after put, position: 5
  • after put, limit: 8
  • after put, capacity: 8
  • ===============> flip
  • after flip, position: 0
  • after flip, limit: 5
  • after flip, capacity: 8
  • ===============> get
  • get result: hel
  • after get, position: 3
  • after get, limit: 5
  • after get, capacity: 8
  • ===============> rewind
  • after rewind, position: 0
  • after rewind, limit: 5
  • after rewind, capacity: 8
  • get result: hel
  • ===============> mark
  • position when mark: 3
  • after mark, position: 3
  • after mark, limit: 5
  • after mark, capacity: 8
  • get result: lo
  • ===============> reset
  • times 0, position before reset: 5
  • times 0, position after reset: 3
  • times 0, get result: lo
  • times 1, position before reset: 5
  • times 1, position after reset: 3
  • times 1, get result: lo
  • times 2, position before reset: 5
  • times 2, position after reset: 3
  • times 2, get result: lo
  • ===============> compact
  • after reset, position: 3
  • after reset, limit: 5
  • after reset, capacity: 8
  • after compact, position: 2
  • after compact, limit: 8
  • after compact, capacity: 8
  • after flip, position: 0
  • after flip, limit: 2
  • after flip, capacity: 8
  • get result: lo
  • ===============> duplicate
  • duplicateByteBuffer, position: 2
  • duplicateByteBuffer, limit: 2
  • duplicateByteBuffer, capacity: 8
  • ===============> duplicate rewind
  • duplicateByteBuffer, position: 0
  • duplicateByteBuffer, limit: 2
  • duplicateByteBuffer, capacity: 8
  • byteBuffer, position: 2
  • byteBuffer, limit: 2
  • byteBuffer, capacity: 8
  • ===============> duplicate modified
  • duplicateByteBuffer String: Ao
  • byteBuffer String: Ao
  • ===============> slice
  • sliceByteBuffer, position: 0
  • sliceByteBuffer, limit: 0
  • sliceByteBuffer, capacity: 0
  • ===============> slice rewind
  • sliceByteBuffer, position: 0
  • sliceByteBuffer, limit: 0
  • sliceByteBuffer, capacity: 0
  • byteBuffer, position: 2
  • byteBuffer, limit: 2
  • byteBuffer, capacity: 8
  • ===============> hasRemaining
  • hasRemaining: false
  • ===============> clear
  • after clear, position: 0
  • after clear, limit: 8
  • after clear, capacity: 8
  • Garbage datas: A
  • Garbage datas: o
  • Garbage datas: l
  • Garbage datas: l
  • Garbage datas: o
  • Garbage datas: x // 这里其实读出来是乱码,无法显示,所以用x表示
  • Garbage datas: x // 这里其实读出来是乱码,无法显示,所以用x表示
  • Garbage datas: x // 这里其实读出来是乱码,无法显示,所以用x表示

以及流程示意图:

19.ByteBuffer状态总览(1).png

20.ByteBuffer状态总览(2).png