Buffer 类是 java.nio 的构造基础。一个 Buffer 对象是固定数量的数据的容器,其作用是一个存储器,或者分段运输区,在这里,数据可被存储并在之后用于检索。缓冲区可以被写满或释放。对于每个非布尔原始数据类型都有一个缓冲区类,即 Buffer 的子类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer,是没有 BooleanBuffer 之说的。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的。
◇ 缓冲区的四个属性
所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息,这四个属性尽管简单,但其至关重要,需熟记于心:
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,这些调用执行完后,position 的值会自动前进。当然,对于 put,如果调用多次导致位置超出上界(注意,是 limit 而不是 capacity),则会抛出 BufferOverflowException 异常;对于 get,如果位置不小于上界(同样是 limit 而不是 capacity),则会抛出 BufferUnderflowException 异常。这种不带索引参数的方法,称为相对存取,相对存取会自动影响缓冲区的位置属性。带索引参数的方法,称为绝对存取,绝对存储不会影响缓冲区的位置属性,但如果你提供的索引值超出范围(负数或不小于上界),也将抛出 IndexOutOfBoundsException 异常。
◇ 翻转
我们把 hello 这个串通过 put 存入一 ByteBuffer 中,如下所示:将 hello 存入 ByteBuffer 中
ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');此时,position = 5,limit = capacity = 1024。现在我们要从正确的位置从 buffer 读数据,我们可以把 position 置为 0,那么字符串的结束位置在哪呢?这里上界该出场了。如果把上界设置成当前 position 的位置,即 5,那么 limit 就是结束的位置。上界属性指明了缓冲区有效内容的末端。
人工实现翻转:
buffer.limit(buffer.position()).position(0);但这种从填充到释放状态的缓冲区翻转是API设计者预先设计好的,他们为我们提供了一个非常便利的函数: buffer.flip()。 另外,rewind() 函数与 flip() 相似,但不影响上界属性,它只是将位置值设回 0。 在进行buffer读操作的时候,一般都会使用buffer.flip()函数。
◇ 释放(Drain)
这里的释放,指的是缓冲区通过 put 填充数据后,然后被读出的过程。上面讲了,要读数据,首先得翻转。那么怎么读呢?hasRemaining() 会在释放缓冲区时告诉你是否已经达到缓冲区的上界:hasRemaining()函数和Remaining()函数有密切的功能,
for (int i = 0; buffer.hasRemaining(); i++) { myByteArray[i] = buffer.get(); }很明显,上面的代码,每次都要判断元素是否到达上界。我们可以做: 改变后的释放过程
int count = buffer.hasRemaining(); for (int i = 0; i < count; i++) { myByteArray[i] = buffer.get(); }第二段代码看起来很高效,但请注意,缓冲区并不是多线程安全的。如果你想以多线程同时存取特定的缓冲区,你需要在存取缓冲区之前进行同步。因此,使用第二段代码的前提是,你对缓冲区有专门的控制。
Compact 之前的缓冲区
buffer.compact() 会使缓冲区的状态图如下图所示:
Compact 之后的缓冲区
这里发生了几件事:
两个被认为是相等的缓冲区
两个被认为是不相等的缓冲区
缓冲区也支持用 compareTo() 函数以词典顺序进行比较,当然,这是所有的缓冲区实现了 java.lang.Comparable 语义化的接口。这也意味着缓冲区数组可以通过调用 java.util.Arrays.sort() 函数按照它们的内容进行排序。
与 equals() 相似,compareTo() 不允许不同对象间进行比较。但 compareTo()更为严格:如果你传递一个类型错误的对象,它会抛出 ClassCastException 异常,但 equals() 只会返回 false。
比较是针对每个缓冲区你剩余数据(从 position 到 limit)进行的,与它们在 equals() 中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。如果一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。这里有个顺序问题:下面小于零的结果(表达式的值为 true)的含义是 buffer2 < buffer1。切记,这代表的并不是 buffer1 < buffer2。
if (buffer1.compareTo(buffer2) < 0) { // do sth, it means buffer2 < buffer1,not buffer1 < buffer2 doSth(); }◇ 批量移动
public abstract class ByteBuffer extends Buffer implements Comparable { public ByteBuffer get(byte[] dst); public ByteBuffer get(byte[] dst, int offset, int length); public final ByteBuffer put(byte[] src); public ByteBuffer put(byte[] src, int offset, int length); }
如你在上面的程序清单中所看到的那样,buffer API 提供了向缓冲区内外批量移动数据元素的函数。以 get 为例,它将缓冲区中的内容复制到指定的数组中,当然是从 position 开始咯。第二种形式使用 offset 和 length 参数来指定复制到目标数组的子区间。这些批量移动的合成效果与前文所讨论的循环是相同的,但是这些方法可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据。
批量移动总是具有指定的长度。也就是说,你总是要求移动固定数量的数据元素。因此,get(dist) 和 get(dist, 0, dist.length) 是等价的。
对于以下几种情况的数据复制会发生异常:
byte[] smallArray = new Byte[10]; while (buffer.hasRemaining()) { int length = Math.min(buffer.remaining(), smallArray.length); buffer.get(smallArray, 0, length); // 每取出一部分数据后,即调用 processData 方法,length 表示实际上取到了多少字节的数据 processData(smallArray, length); }put() 的批量版本工作方式相似,只不过它是将数组里的元素写入 buffer 中而已,这里不再赘述。
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(); }
新的缓冲区是由分配(allocate)或包装(wrap)操作创建的。分配(allocate)操作创建一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素。包装(wrap)操作创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用你所提供的数组作为存储空间来储存缓冲区中的数据元素。demos:
Java代码
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(); }● duplidate()
CharBuffer buffer = CharBuffer.allocate(8); buffer.position(3).limit(6).mark().position (5); CharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();
复制一个缓冲区
● asReadOnlyBuffer()
asReadOnlyBuffer() 函数来生成一个只读的缓冲区视图。这与duplicate() 相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly() 函数将会返回 true。
如果一个只读的缓冲区与一个可写的缓冲区共享数据,或者有包装好的备份数组,那么对这个可写的缓冲区或直接对这个数组的改变将反映在所有关联的缓冲区上,包括只读缓冲区。
● slice()
分割缓冲区与复制相似,但 slice() 创建一个从原始缓冲区的当前 position 开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量(limit - position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。slice() 分割缓冲区:
Java代码
package java.nio; public final class ByteOrder { public static final ByteOrder BIG_ENDIAN; public static final ByteOrder LITTLE_ENDIAN; public static ByteOrder nativeOrder(); public String toString(); }ByteOrder 类定义了决定从缓冲区中存储或检索多字节数值时使用哪一字节顺序的常量。如果你需要知道 JVM 运行的硬件平台的固有字节顺序,请调用静态类函数 nativeOrder()。
public abstract class CharBuffer extends Buffer implements Comparable, CharSequence { // This is a partial API listing public final ByteOrder order(); }这个函数从 ByteOrder 返回两个常量之一。对于除了 ByteBuffer 之外的其他缓冲区类,字节顺序是一个只读属性,并且可能根据缓冲区的建立方式而采用不同的值。除了 ByteBuffer,其他通过 allocate() 或 wrap() 一个数组所创建的缓冲区将从 order() 返回与 ByteOrder.nativeOrder() 相同的数值。这是因为包含在缓冲区中的元素在 JVM 中将会被作为基本数据直接存取。
public abstract class ByteBuffer extends Buffer implements Comparable { // This is a partial API listing public final ByteOrder order(); public final ByteBuffer order(ByteOrder bo); }如果一个缓冲区被创建为一个 ByteBuffer 对象的视图,,那么 order() 返回的数值就是视图被创建时其创建源头的 ByteBuffer 的字节顺序。视图的字节顺序设定在创建后不能被改变,而且如果原始的字节缓冲区的字节顺序在之后被改变,它也不会受到影响。
I/O 缓冲区操作简图
从图中你可能会觉得,把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?首先,硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色。
因此,操作系统是在内存区域中进行 I/O 操作。这些内存区域,就操作系统方面而言,是相连的字节序列,这也意味着I/O操作的目标内存区域必须是连续的字节序列。在 JVM中,字节数组可能不会在内存中连续存储(因为 JAVA 有 GC 机制),或者无用存储单元(会被垃圾回收)收集可能随时对其进行移动。
出于这个原因,引入了直接缓冲区的概念。直接字节缓冲区通常是 I/O 操作最好的选择。非直接字节缓冲区(即通过 allocate() 或 wrap() 创建的缓冲区)可以被传递给通道,但是这样可能导致性能损耗。通常非直接缓冲不可能成为一个本地 I/O 操作的目标。
如果你向一个通道中传递一个非直接 ByteBuffer 对象用于写入,通道可能会在每次调用中隐含地进行下面的操作:
public abstract class ByteBuffer extends Buffer implements Comparable { // This is a partial API listing public static ByteBuffer allocateDirect (int capacity); public abstract boolean isDirect(); }所有的缓冲区都提供了一个叫做 isDirect() 的 boolean 函数,来测试特定缓冲区是否为直接缓冲区。但是, ByteBuffer 是唯一可以被分配成直接缓冲区的 Buffer。 尽管如此,如果基础缓冲区是一个直接 ByteBuffer,对于非字节视图缓冲区,isDirect() 可以是 true。
public abstract class ByteBuffer extends Buffer implements Comparable { // This is a partial API listing public abstract CharBuffer asCharBuffer(); public abstract CharBuffer asShortBuffer( ); public abstract CharBuffer asIntBuffer( ); public abstract CharBuffer asLongBuffer( ); public abstract CharBuffer asFloatBuffer( ); public abstract CharBuffer asDoubleBuffer( ); }下面的代码创建了一个 ByteBuffer 缓冲区的 CharBuffer 视图。 演示 7 个字节的 ByteBuffer 的 CharBuffer 视图:
** * 1 char = 2 byte,因此 7 个字节的 ByteBuffer 最终只会产生 capacity 为 3 的 CharBuffer。 * * 无论何时一个视图缓冲区存取一个 ByteBuffer 的基础字节,这些字节都会根据这个视图缓冲区的字节顺序设 * 定被包装成一个数据元素。当一个视图缓冲区被创建时,视图创建的同时它也继承了基础 ByteBuffer 对象的 * 字节顺序设定,这个视图的字节排序不能再被修改。字节顺序设定决定了这些字节对是怎么样被组合成字符 * 型变量的,这样可以理解为什么 ByteBuffer 有字节顺序的概念了吧。 */ ByteBuffer byteBuffer = ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN); CharBuffer charBuffer = byteBuffer.asCharBuffer();
7 个 字节的 ByteBuffer 的 CharBuffer 视图
◇ 数据元素视图
ByteBuffer 类为每一种原始数据类型提供了存取的和转化的方法:
public abstract class ByteBuffer extends Buffer implements Comparable { public abstract short getShort( ); public abstract short getShort(int index); public abstract short getInt( ); public abstract short getInt(int index); ...... public abstract ByteBuffer putShort(short value); public abstract ByteBuffer putShort(int index, short value); public abstract ByteBuffer putInt(int value); public abstract ByteBuffer putInt(int index, int value); ....... }这些函数从当前位置开始存取 ByteBuffer 的字节数据,就好像一个数据元素被存储在那里一样。根据这个缓冲区的当前的有效的字节顺序,这些字节数据会被排列或打乱成需要的原始数据类型。
// 大端顺序 int value = buffer.order(ByteOrder.BIG_ENDIAN).getInt(); // 小端顺序 int value = buffer.order(ByteOrder.LITTLE_ENDIAN).getInt(); // 上述两种方法取得的 int 是不一样的,因此在调用此类方法前,请确保字节顺序是你所期望的如果你试图获取的原始类型需要比缓冲区中存在的字节数更多的字节,会抛出 BufferUnderflowException。