NIO 之 Buffer 图解

可参考之前的文章:NIO 之 ByteBuffer实现原理
下面是对之前文章的一个补充

Buffer 类 结构

对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。

概述

缓冲区 Buffer 内部就是用数组实现的。 Buffer 包含了下面4个属性:

  • 容量( Capacity)
    缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
  • 上界( Limit)
    缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
  • 位置( Position)
    下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
  • 标记( Mark)
    一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position = mark。标记在设定前是未定义的(undefined)。

这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity

示例

下面展示了一个新创建的容量为 10 的 ByteBuffer 逻辑视图

ByteBuffer.allocate(10);
图1

位置(Position)被设为 0,而且容量( Capacity)和上界( Limit)被设为 10,刚好经过缓冲区能够容纳的最后一个字节。
标记(mark)最初未定义。
容量(Capacity)是固定的,但另外的三个属性可以在使用缓冲区时改变。

put() 方法

让我们看一个例子。 我们将代表“abcde”字符串的 ASCII 码载入一个名为 buffer 的
ByteBuffer 对象中。当在图1 中所新建的缓冲区上执行以下代码后。

buffer.put((byte)'a').put((byte)'b').put((byte)'c').put((byte)'d').put((byte)'e');

缓冲区的结果状态如图 2所示:


图2

flip() 方法

我们已经写满了缓冲区,现在我们必须准备将其清空。我们想把这个缓冲区传递给一个通
道,以使内容能被全部写出。但如果通道现在在缓冲区上执行 get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据。如果我们将位置值重新设为 0,通道就会从正确位置开始获取,但是它是怎样知道何时到达我们所插入数据末端的呢?这就是上界属性被引入的目的。上界属性指明了缓冲区有效内容的末端。我们需要将上界属性设置为当前位置,然后将位置重置为 0。

flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素
的释放状态。在翻转之后,图 2 的缓冲区会变成图 3 中的样子。

图3

rewind() 方法

rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。您可以使
用 rewind()后退,重读已经被翻转的缓冲区中的数据。
图2 的缓冲区调用 rewind() 方法会变成图4 中的样子。


图4

如果将缓冲区翻转两次会怎样呢?

compact() 方法

有时,您可能只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这
一点,未读的数据元素需要下移以使第一个元素索引为 0。尽管重复这样做会效率低下,但这有时非常必要,而 API 对此为您提供了一个 compact()函数。这一缓冲区工具在复制数据时要比您使用 get()和 put()函数高效得多。所以当您需要时,请使用 compact()。图 5显示了一个读取了两个元素(position 现在为2),并且现在我们想要对其进行压缩的缓冲区。

图5
buffer.compact();

压缩后的结果如下图


图6

duplicate() 方法

duplicate() 方法创建了一个与原始缓冲区一样的新缓冲区。两个缓冲区共享数据,拥有同样的 capacity ,但每个缓冲区都拥有自己的 position,limit 和 mark 属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。

    public ByteBuffer duplicate() {
        return new HeapByteBufferR(hb,
                                        this.markValue(),
                                        this.position(),
                                        this.limit(),
                                        this.capacity(),
                                        offset);
    }

重新创建一个 ByteBuffer,并且使用同一个数组。所有一个byteBuffer 变动,会影响另一个 ByteBuffer。 但 position、limit、mark 都是独立的。

duplicate() 方法

您 可 以 使 用 asReadOnlyBuffer() 函 数 来 生 成 一 个 只 读 的 缓 冲 区 视 图 。 这 与
duplicate()相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly()函数
将 会 返 回 true 。 对 这 一 只 读 缓 冲 区 的 put() 函 数 的 调 用 尝 试 会 导 致 抛 出
ReadOnlyBufferException 异常。

public ByteBuffer asReadOnlyBuffer() {
        return new HeapByteBufferR(hb,
                                     this.markValue(),
                                     this.position(),
                                     this.limit(),
                                     this.capacity(),
                                     offset);
}
HeapByteBufferR 分析
class HeapByteBufferR
    extends HeapByteBuffer{
    public ByteBuffer put(byte x) {
        throw new ReadOnlyBufferException();
    }

    public ByteBuffer put(int i, byte x) {
        throw new ReadOnlyBufferException();
    }

    public ByteBuffer putInt(int x) {
        throw new ReadOnlyBufferException();
    }
    ......
}

HeapByteBufferR 继承 HeapByteBuffer 类,并重写了所有的可修改 buffer 的方法。把所有能修改 buffer 的方法都直接 throw ReadOnlyBufferException,来保证只读。

slice() 方法

slice() 分割缓冲区。创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量( limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。

原 ByteBuffer如下图:


slice() 分割后的 ByteBuffer


    public ByteBuffer slice() {
        return new HeapByteBuffer(hb,
                                        -1,
                                        0,
                                        this.remaining(),
                                        this.remaining(),
                                        this.position() + offset);
    }

喜欢本文的朋友们,欢迎长按下图关注订阅号 java404,收听更多精彩的内容

java404

推荐阅读更多精彩内容