「高并发通信框架Netty4 源码解读(三)」NIO缓冲区Buffer详解

时间:2022-07-24
本文章向大家介绍「高并发通信框架Netty4 源码解读(三)」NIO缓冲区Buffer详解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

我们以 Buffer 类开始我们对 java.nio 软件包的浏览历程。这些类是 java.nio 的构基础。我们将深入研究缓冲区, 了解各种不同的类型,并学会怎样使用。到那时我们将明了 java.nio 缓冲区是如何与 java.nio.channels 这一通道类相联系的。

1.前言

一个Buffer对象是固定数量的数据的容器。其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。缓冲区如我们在上一篇所讨论的那样被写满和释放。对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的。

缓冲区的工作与通道紧密联系。通道是 I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,您想传递出去的数据被置于一个缓冲区,被传送到通道。对于传回缓冲区的传输,一个通道将数据放置在您所提供的缓冲区中。这种在协同对象(通常是您所写的对象以及一到多个 Channel 对象)之间进行的缓冲区数据传递是高效数据处理的关键。 下一篇博客详解通道。

下图是 Buffer 的类层次图。在顶部是通用 Buffer 类。 Buffer 定义所有缓冲区类型共有的操作,无论是它们所包含的数据类型还是可能具有的特定行为。这一共同点将会成为我们的出发点。

2.缓冲区基础

概念上,缓冲区是包在一个对象内的基本数据元素数组。 Buffer 类相比一个简单数组的优点是它将关于数据的数据内容和信息包含在一个单一的对象中。 Buffer 类以及它专有的子类定义了 一个用于处理数据缓冲区的 API。

2.1 属性

所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。它们是:

  • 容量(Capacity) 缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
  • 上界(Limit) 缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
  • 位置(Position) 下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
  • 标记(Mark) 一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。标记在设定前是未定义的(undefined)。 这四个属性之间总是遵循以下关系:0 <= mark <= position <= limit <= capacity,让我们来看看这些属性在实际应用中的一些例子。下图展示了一个新创建的容量为 10的 ByteBuffer 逻辑视图。

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

2.2 缓冲区 API

package java.nio;
public abstract class Buffer {
public final int capacity( )
public final int position( )
public final Buffer position (int newPositio
public final int limit( )
public final Buffer limit (int newLimit)
public final Buffer mark( )
public final Buffer reset( )
public final Buffer clear( )
public final Buffer flip( )
public final Buffer rewind( )
public final int remaining( )
public final boolean hasRemaining( )
public abstract boolean isReadOnly( );
}

关于这个 API 有一点要注意的是,像 clear()这类函数,您通常应当返回 void,而不是 Buffer 引用。这些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级 联调用的类设计方法。级联调用允许这种类型的代码:

buffer.mark( );
buffer.position(5);
buffer.reset( );

被简写为:

buffer.mark().position(5).reset( );

java.nio 中的类被特意地设计为支持级联调用。您可能已经在 StringBuffer 类中看到了级联调用的使用。如果聪明地使用级联调用, 就能产生简洁,优美, 易读的代码。 但如果滥用,就会使代码不知所云。 当级联调用可以增加可读性并使让您的目标更加明确时使用它。如果使用级联调用会使代码作用不够清晰,那么请不要使用它。请时刻保证您的代码易于他人阅读。

对于 API 还要注意的一点是 isReadOnly()函数。所有的缓冲区都是可读的,但并非所有都可写。每个具体的缓冲区类都通过执行 isReadOnly()来标示其是否允许该缓存区的内容 被 修 改 。 一 些 类 型 的 缓 冲 区 类 可 能 未 使 其 数 据 元 素 存 储 在 一 个 数 组 中 。 例 如MappedByteBuffer 的内容可能实际是一个只读文件。您也可以明确地创建一个只读视图缓冲 区 , 来 防 止 对 内 容 的 意 外 修 改 。 对 只 读 的 缓 冲 区 的 修 改 尝 试 将 会 导 致ReadOnlyBufferException 抛出。但是我们要提前做好准备。

2.3存取

让我们从起点开始。缓冲区管理着固定数目的数据元素。但在任何特定的时刻,我们可能只对缓冲区中的一部分元素感兴趣。换句话说,在我们想清空缓冲区之前,我们可能只使用了缓冲区的一部分。这时,我们需要能够追踪添加到缓冲区内的数据元素的数量,放入下一个元素的位置等等的方法。位置属性做到了这一点。它在调用 put()时指出了下一个数据元素应该被插入的位置,或者当 get()被调用时指出下一个元素应从何处检索。聪明的读者会注意到上文所列出的的 Buffer API 并没有包括 get()或 put()函数。每一个 Buffer 类都有这两个函数,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以它们不能在顶层 Buffer 类中被抽象地声明。它们的定义必须被特定类型的子类所遵从。对于这一讨论,我们将假设使用具有这里所给出的函数的 ByteBuffer 类。

public abstract class ByteBuffer
extends Buffer implements Comparable
{
// This is a partial API listing
public abstract byte get( );
public abstract byte get (int index);
public abstract ByteBuffer put (byte b);
public abstract ByteBuffer put (int index, byte b);
}

Get 和 put 可以是相对的或者是绝对的。在前面的程序列表中,相对方案是不带有索引参数的函数。当相对函数被调用时,位置在返回时前进一。如果位置前进过多,相对运算就会抛 出 异 常 。 对于put(),如果运 算会导致位置超出上界 , 就会抛出BufferOverflowException 异常。对于 get(),如果位置不小于上界,就会抛出 BufferUnderflowException 异常。绝对存取不会影响缓冲区的位置属性,但是如果您所提供的索引超出范围(负数或不小于上界),也将抛出 IndexOutOfBoundsException 异常。

2.4填充

让我们看一个例子。 我们将代表“Hello”字符串的 ASCII 码载入一个名为 buffer 的ByteBuffer 对象中。所有类型底层维护一个数组,比如ByteBuffer维护的数据如下:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{

    final byte[] hb;                  // 底层维护的字节数组
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers
.......省略

当在上文中的图所新建的缓冲区上执行以下代码后,缓冲区的结果状态如下图所示: buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');

注意本例中的每个字符都必须被强制转换为 byte。我们不能不经强制转换而这样操做:buffer.put('H');

因为我们存放的是字节而不是字符。记住在 java 中,字符在内部以 Unicode 码表示,每个 Unicode 字符占 16 位。例子使用包含 ascii 字符集数值的字节。通过将char 强制转换为 byte,我们删除了前八位来建立一个八位字节值。这通常只适合于拉丁字符而不能适合所有可能的 Unicode 字符。为了让事情简化,我们暂时故意忽略字符集的映射问题。以后将详细涉及字符编码。

既然我们已经在 buffer 中存放了一些数据,如果我们想在不丢失位置的情况下进行一些更改该怎么办呢? put()的绝对方案可以达到这样的目的。假设我们想将缓冲区中的内容从“Hello”的 ASCII 码更改为“Mellow”。我们可以这样实现:

buffer.put(0,(byte)'M').put((byte)'w');

这里通过进行一次绝对方案的 put 将 0 位置的字节代替为十六进制数值 0x4d,将 0x77放入当前位置(当前位置不会受到绝对 put()的影响)的字节,并将位置属性加一。结果如下图所示:

2.4翻转

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

buffer.limit(buffer.position()).position(0);

但这种从填充到释放状态的缓冲区翻转是 API 设计者预先设计好的,他们为我们提供了一个非常便利的函数:flip(),Flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素 的释放状态。在翻转之后,上图变成下图这样:

我们看下flip函数的源码:

public final Buffer flip() {
        limit = position;//位置元素赋值给上限元素
        position = 0;//位置元素设置为0
        mark = -1;
        return this;
    }

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

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

2.4释放

如果我们现在将上图中的缓冲区传入通道,它将取出我们存放在那里的数据,从位置开始直到上界结束。很简单,不是吗? 同样地,如果您接收到一个在别处被填满的缓冲区,您可能需要在检索内容之前将其翻转。例如,如果一个通道的 read()操作完成,而您想要查看被通道放入缓冲区内的数据,那么您需要在调用 get()之前翻转缓冲区。通道对象在缓冲区上调用 put()增加数据;put和read 可以随意混合使用。 布尔函数 hasRemaining()会在释放缓冲区时告诉您是否已经达到缓冲区的上界。源码如下:

  public final boolean hasRemaining() {
        return position < limit;//很简单
    }

以下是一种将数据元素从缓冲区释放到一个数组的方法。

for (int i = 0; buffer.hasRemaining( );i++) {
    myByteArray [i] = buffer.get( );
}

作为选择, remaining()函数将告知您从当前位置到上界还剩余的元素数目。 您也可以通过下面的循环来释放缓冲区。

int count = buffer.remaining( );
for (int i = 0; i < count, i++) {
    myByteArray [i] = buffer.get( );
}

//remaining源码
 public final int remaining() {
        return limit - position;
    }

一旦缓冲区对象完成填充并释放,它就可以被重新使用了。 Clear()函数将缓冲区重置为空状态。 它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回 0,这使得缓冲区可以被重新填入。

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

看一个完整的案例:填充和释放缓冲区

package com.ronsoft.books.nio.buffers;
import java.nio.CharBuffer;
/**
* Buffer fill/drain example. This code uses the simplest
* means of filling and draining a buffer: one element at
* a time.
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class BufferFillDrain
{

    private static int index = 0;
    private static String[] strings = {
            "A random string value",
            "The product of an infinite number of monkeys",
            "Hey hey we're the Monkees",
            "Opening act for the Monkees: Jimi Hendrix",
            "'Scuse me while I kiss this fly", // Sorry Jimi ;-)
            "Help Me! Help Me!",
    };

     public static void main(String[] argv) throws Exception {
        CharBuffer buffer = CharBuffer.allocate(100);
        while (fillBuffer(buffer)) {
            buffer.flip();
            drainBuffer(buffer);
            buffer.clear();
        }
    }

    private static void drainBuffer(CharBuffer buffer) {
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }
        System.out.println("");
    }

    private static boolean fillBuffer(CharBuffer buffer) {
        if (index >= strings.length) {
            return (false);
        }
        String string = strings[index++];
        for (int i = 0; i < string.length(); i++) {
            buffer.put(string.charAt(i));
        }
        return (true);
    }
}

2.5压缩

public abstract class ByteBuffer
extends Buffer implements Comparable
{
  // This is a partial API listing
  public abstract ByteBuffer compact( );
}

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

这样操作:

buffer.compact();

会导致缓冲区的状态如下图

这里发生了几件事。您会看到数据元素 2-5 被复制到 0-3 位置。位置 4 和 5 不受影响,但现在正在或已经超出了当前位置,因此是“死的”。它们可以被之后的 put()调用重写。还要注意的是,位置已经被设为被复制的数据元素的数目。也就是说,缓冲区现在被定位在缓冲区中最后一个“存活”元素后插入数据的位置。最后,上界属性被设置为容量的值,因此缓冲区可以被再次填满。调用 compact()的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪。源码如下

  public ByteBuffer compact() {
      //将未读出的元素拷贝到前面,
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
    }

如果您想在压缩后释放数据,缓冲区会像之前所讨论的那样需要被翻转。无论您之后是否要向缓冲区中添加新的数据,这一点都是必要的。

2.6标记

mark()标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在 mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。 reset( )函数将位置设为当前的标记值。如果标记值未定义,调用 reset( )将导致 InvalidMarkException 异常。一些缓冲区函数会抛弃已经设定的标记( rewind( ), clear( ),以及 flip( )总是抛弃标记,即mark=-1)。如果新设定的值比当前的标记小,调用limit( )或 position( )带有索引参数的版本会抛弃标记。

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

抛弃标记

//所谓的抛弃,就是设置mark=-1啦
public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;//抛弃标记
        return this;
    }

2.7比较

有时候比较两个缓冲区所包含的数据是很有必要的。所有的缓冲区都提供了一个常规的equals( )函数用以测试两个缓冲区的是否相等,以及一个 compareTo( )函数用以比较缓冲区。

public abstract class ByteBuffer
extends Buffer implements Comparable
{
  // This is a partial API listing
  public boolean equals (Object ob)
  public int compareTo (Object ob)
}

两个缓冲区可用下面的代码来测试是否相等:

if (buffer1.equals (buffer2)) {
  doSomething( );
}

//equals 源码,只比较剩余元素是否相等。

    public boolean equals(Object ob) {
        if (this == ob)//同一个对象,肯定相等
            return true;
        if (!(ob instanceof ByteBuffer))//不是同一个类型,肯定不相等
            return false;
        ByteBuffer that = (ByteBuffer)ob;
        if (this.remaining() != that.remaining())//剩余数量是否相等
            return false;
        int p = this.position();
        for (int i = this.limit() - 1, j = that.limit() - 1; i >= p; i--, j--)//剩余元素是否相等
            if (!equals(this.get(i), that.get(j)))
                return false;
        return true;
    }

如果每个缓冲区中剩余的内容相同,那么 equals( )函数将返回 true,否则返回 false。因为这个测试是用于严格的相等而且是可换向的。前面的程序清单中的缓冲区名称可以颠倒,并会产生相同的结果。 缓冲区也支持用 compareTo( )函数以词典顺序进行比较。这一函数在缓冲区参数小于,等于,或者大于引用 compareTo( )的对象实例时,分别返回一个负整数, 0 和正整数。这些就是所有典型的缓冲区所实现的 java.lang.Comparable 接口语义。这意味着缓冲区数组可以通过调用 java.util.Arrays.sort()函数按照它们的内容进行排序。 equals( )相似, compareTo( )不允许不同对象间进行比较。但 compareTo( )更为严格:如果您传递一个类型错误的对象,它会抛出 ClassCastException 异常,但 equals( )只会返回false。比较是针对每个缓冲区内剩余数据进行的,与它们在 equals( )中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。如果一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。不像 equals( ), compareTo( )不可交换:顺序问题。在本例中,一个小于零的结果表明 buffer2 小于 buffer1,而表达式的值就会是 true:

if (buffer1.compareTo (buffer2) < 0) {
doSomething( );
}

源码

   public int compareTo(ByteBuffer that) {
        int n = this.position() + Math.min(this.remaining(), that.remaining());//当前对象应读取的长度
        for (int i = this.position(), j = that.position(); i < n; i++, j++) {
            int cmp = compare(this.get(i), that.get(j));//比较是否相等
            if (cmp != 0)//不相等
                return cmp;
        }
//比较剩余个数,this多说明this的剩余数包含了that的剩余数,
        return this.remaining() - that.remaining();且包含的内容相等
    }

2.8批量移动

缓冲区的涉及目的就是为了能够高效传输数据。一次移动一个数据元素并不高效。如您在下面的程序清单中所看到的那样, buffer API 提供了向缓冲区内外批量移动数据元素的函数。

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
  // This is a partial API listing
  public CharBuffer get (char [] dst)
  public CharBuffer get (char [] dst, int offset, int length)
  public final CharBuffer put (char[] src)
  public CharBuffer put (char [] src, int offset, int length)
  public CharBuffer put (CharBuffer src)
  public final CharBuffer put (String src)
  public CharBuffer put (String src, int start, int end)
}

有两种形式的 get( )可供从缓冲区到数组进行的数据复制使用。第一种形式只将一个数组作为参数,将一个缓冲区释放到给定的数组。第二种形式使用 offset 和 length 参数来指定目标数组的子区间。这些批量移动的合成效果与前文所讨论的循环是相同的,但是这些方法可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据。批量移动总是具有指定的长度。也就是说,您总是要求移动固定数量的数据元素。当参看程序签名时这一点还不明显,但是对 get( )的这一引用: buffer.get(myArray); 等价于: buffer.get(myArray,0,myArray.length); 批量传输的大小总是固定的。省略长度意味着整个数组会被填满。 如果您所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不变,同时抛出 BufferUnderflowException 异常。因此当您传入一个数组并且没有指定长度,您就相当于要求整个数组被填充。如果缓冲区中的数据不够完全填满数组,您会得到一个异常。这意味着如果您想将一个小型缓冲区传入一个大型数组,您需要明确地指定缓冲区中剩余的数据长度。上面的第一个例子不会如您第一眼所推出的结论那样,将缓冲区内剩余的数据元素复制到数组的底部。要将一个缓冲区释放到一个大数组中,要这样做:

char [] bigArray = new char [1000];
// 获取剩余元素数量
int length = buffer.remaining( );
buffer.get (bigArrray, 0, length);
// 处理
processData (bigArray, length);

另一方面,如果缓冲区存有比数组能容纳的数量更多的数据,您可以重复利用如下文所示的程序块进行读取:

char [] smallArray = new char [10];
while (buffer.hasRemaining( )) {
  int length = Math.min (buffer.remaining( ), smallArray.length);
  buffer.get (smallArray, 0, length);
  processData (smallArray, length);
}

Put()的批量版本工作方式相似,但以相反的方向移动数据,从数组移动到缓冲区。他们在传送数据的大小方面有着相同的语义: buffer.put(myArray); 等价于: buffer.put(myArray,0,myArray.length); 如果缓冲区有足够的空间接受数组中的数据( buffer.remaining()>myArray.length),数据将会被复制到从当前位置开始的缓冲区,并且缓冲区位置会被提前所增加数据元素的数量。如果缓冲区中没有足够的空间,那么不会有数据被传递,同时抛出一个 BufferOverflowException 异常。也可以通过调用带有一个缓冲区引用作为参数的 put()来在两个缓冲区内进行批量传递。 buffer.put(srcBuffer); 这等价于(假设 dstBuffer 有足够的空间):

while (srcBuffer.hasRemaining( )) {
dstBuffer.put (srcBuffer.get( ));
}

3创建缓冲区

上面讲过有七种主要的缓冲区类,每一种都具有一种 Java 语言中的非布尔类型的原始类型数据。(第 8 种也在图中显示出来, MappedByteBuffer,是ByteBuffer 专门用于内存映射文件的一种特例。我们将会在通道内容时讨论内存映射)。这些类没有一种能够直接实例化。它们都是抽象类,但是都包含静态工厂方法用来创建相应类的新实例。 对于这一讨论,我们将以 CharBuffer 类为例,但是对于其它六种主要的缓冲区类也是适用的: IntBuffer, DoubleBuffer, ShortBuffer, LongBuffer, FloatBuffer和 ByteBuffer。下面是创建一个缓冲区的关键函数,对所有的缓冲区类通用(要按照需要替换类名):

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
    // This is a partial API listing
    public static CharBuffer allocate (int capacity)
    public static CharBuffer wrap (char [] array)
    public static CharBuffer wrap (char [] array, int offset,int length)
    public final boolean hasArray( )
    public final char [] array( )
    public final int arrayOffset( )
}

新的缓冲区是由分配或包装操作创建的。分配操作创建一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素。包装操作创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用您所提供的数组作为存储空间来储存缓冲区中的数据元素。要分配一个容量为 100 个 char 变量的 Charbuffer: CharBuffer charBuffer = CharBuffer.allocate (100);,我们看他的构造函数:

HeapCharBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new char[cap], 0);
 }
CharBuffer(int mark, int pos, int lim, int cap,   // package-private
                 char[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

其实就是在底层生成了一个char数组,并设置了mark position limit capacity属性。大家能看到,这个Buffer就是围绕数组展开的。 如果您想提供您自己的数组用做缓冲区的备份存储器,请调用 wrap()函数: char [] myArray = new char [100]; CharBuffer charbuffer = CharBuffer.wrap (myArray); wrap底层源码:

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

public static CharBuffer wrap(char[] array,
                                    int offset, int length)
    {
        try {
            return new HeapCharBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }

还是维护了一个数组,只不过这个数组不是自动生成的,而是程序员提供的。

通过 allocate()或者 wrap()函数创建的缓冲区通常都是间接的(直接缓冲区下一篇博文讲)。间接的缓冲区使用备份数组,像我们之前讨论的,您可以通过上面列出的API 函数获得对这些数组的存取权。 Boolean 型函数 hasArray()告诉您这个缓冲区是否有一个可存取的备份数组。如果这个函数的返回 true, array()函数会返回这个缓冲区对象所使用的数组存储空间的引用。如果 hasArray()函数返回 false,不要调用 array()函数或者 arrayOffset()函数。如果您这样做了您会得到一个UnsupportedOperationException 异常。如果一个缓冲区是只读的,它的备份数组将会是超出上界的,即使一个数组对象被提供给 wrap()函数。调用 array()函数或者 arrayOffset()会抛出一个 ReadOnlyBufferException 异常,来阻止您得到存取权来修改只读缓冲区的内容。如果您通过其它的方式获得了对备份数组的存取权限,对这个数组的修改也会直接影响到这个只读缓冲区。

最后一个函数, arrayOffset(),返回缓冲区数据在数组中存储的开始位置的偏移量(从数组头 0 开始计算)。如果您使用了带有三个参数的版本的 wrap()函数来创建一个缓冲区,对于这个缓冲区, arrayOffset()会一直返回 0,像我们之前讨论的那样。然而,如果您切分了由一个数组提供存储的缓冲区,得到的缓冲区可能会有一个非 0 的数组偏移量。这个数组偏移量和缓冲区容量值会告诉您数组中哪些元素是被缓冲区使用的,这个马上会讲到。

4.复制缓冲区

如我们刚刚所讨论的那样,可以创建描述从外部存储到数组中的数据元素的缓冲区对象。但是缓冲区不限于管理数组中的外部数据。它们也能管理其他缓冲区中的外部数据。当一个管理其他缓冲器所包含的数据元素的缓冲器被创建时,这个缓冲器被称为视图缓冲器。大多数的视图缓冲器都是 ByteBuffer的视图。在继续前往字节缓冲器的细节之前,我们先将注意力放在所有存储器类型的共同视图上。 视图存储器总是通过调用已存在的存储器实例中的函数来创建。使用已存在的存储器实例中的工厂方法意味着视图对象为原始存储器的内部实现细节私有。数据元素可以直接存取,无论它们是存储在数组中还是以一些其他的方式,而不需经过原始缓冲区对象的 get()/put()API。如果原始缓冲区是直接缓冲区,该缓冲区的视图会具有同样的效率优势。映像缓冲区也是如此。

public abstract class CharBuffer
  extends Buffer implements CharSequence, Comparable
{
    // This is a partial API listing
    public abstract CharBuffer duplicate( );
    public abstract CharBuffer asReadOnlyBuffer( );
    public abstract CharBuffer slice( );
}

Duplicate()函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。

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

看源码一目了然,比上面讲一大堆都好用,duplicate(),简单讲,就是生成一个新的Buffer对象,这个buffer对象引用了原对象底层数组的引用。但彼此维护着各自的mark position limit capacity offset等属性,其实,对底层数组的写操作是线程不安全的,大家要注意。

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

    public CharBuffer asReadOnlyBuffer() {

        return new HeapCharBufferR(hb,
                                     this.markValue(),
                                     this.position(),
                                     this.limit(),
                                     this.capacity(),
                                     offset);
    }

HeapCharBufferR继承了HeapCharBuffer类,这个类将isReadOnly属性设置为true

    protected HeapCharBufferR(char[] buf,
                                   int mark, int pos, int lim, int cap,
                                   int off)
    {
        super(buf, mark, pos, lim, cap, off);
        this.isReadOnly = true;

    }

一目了然,多吗?

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

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