Buffer 类是 java.nio 的构造基础。一个 Buffer 对象是固定数量的数据的容器,其作用是一个存储器,或者分段运输区,在这里,数据可被存储并在之后用于检索。缓冲区可以被写满或释放。对于每个非布尔原始数据类型都有一个缓冲区类,即 Buffer 的子类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer,是没有 BooleanBuffer 之说的。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的。
◇ 缓冲区的四个属性
所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息,这四个属性尽管简单,但其至关重要,需熟记于心:
- 容量(Capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
- 上界(Limit):缓冲区的第一个不能被读或写的元素。缓冲创建时,limit 的值等于 capacity 的值。假设 capacity = 1024,我们在程序中设置了 limit = 512,说明,Buffer 的容量为 1024,但是从 512 之后既不能读也不能写,因此可以理解成,Buffer 的实际可用大小为 512。
- 位置(Position):下一个要被读或写的元素的索引。位置会自动由相应的 get() 和 put() 函数更新。 这里需要注意的是positon的位置是从0开始的。
- 标记(Mark):一个备忘位置。标记在设定前是未定义的(undefined)。使用场景是,假设缓冲区中有 10 个元素,position 目前的位置为 2(也就是如果get的话是第三个元素),现在只想发送 6 - 10 之间的缓冲数据,此时我们可以 buffer.mark(buffer.position()),即把当前的 position 记入 mark 中,然后 buffer.postion(6),此时发送给 channel 的数据就是 6 - 10 的数据。发送完后,我们可以调用 buffer.reset() 使得 position = mark,因此这里的 mark 只是用于临时记录一下位置用的。
请切记,在使用 Buffer 时,我们实际操作的就是这四个属性的值。我们发现,Buffer 类并没有包括 get() 或 put() 函数。但是,每一个Buffer 的子类都有这两个函数,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以它们不能在顶层 Buffer 类中被抽象地声明。它们的定义必须被特定类型的子类所遵从。若不加特殊说明,我们在下面讨论的一些内容,都是以 ByteBuffer 为例,当然,它当然有 get() 和 put() 方法了。
◇ 相对存取和绝对存取
Java代码
- 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 中
Java代码
- 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 就是结束的位置。上界属性指明了缓冲区有效内容的末端。人工实现翻转:
Java代码
- buffer.limit(buffer.position()).position(0);
但这种从填充到释放状态的缓冲区翻转是API设计者预先设计好的,他们为我们提供了一个非常便利的函数:buffer.flip()。另外,rewind() 函数与 flip() 相似,但不影响上界属性,它只是将位置值设回 0。在进行buffer读操作的时候,一般都会使用buffer.flip()函数。
◇ 释放(Drain)
这里的释放,指的是缓冲区通过 put 填充数据后,然后被读出的过程。上面讲了,要读数据,首先得翻转。那么怎么读呢?hasRemaining() 会在释放缓冲区时告诉你是否已经达到缓冲区的上界:hasRemaining()函数和Remaining()函数有密切的功能,
Java代码
- for (int i = 0; buffer.hasRemaining(); i++) {
- myByteArray[i] = buffer.get();
- }
很明显,上面的代码,每次都要判断元素是否到达上界。我们可以做:改变后的释放过程
Java代码
- int count = buffer.hasRemaining();
- for (int i = 0; i < count; i++) {
- myByteArray[i] = buffer.get();
- }
第二段代码看起来很高效,但请注意,缓冲区并不是多线程安全的。如果你想以多线程同时存取特定的缓冲区,你需要在存取缓冲区之前进行同步。因此,使用第二段代码的前提是,你对缓冲区有专门的控制。
◇ buffer.clear()
clear() 函数将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将 limit 设为容量的值,并把 position 设回 0。
◇ Compact(不知咋翻译,压缩?紧凑?)
有时候,我们只想释放出一部分数据,即只读取部分数据。当然,你可以把 postion 指向你要读取的第一个数据的位置,将 limit 设置成最后一个元素的位置 + 1。但是,一旦缓冲区对象完成填充并释放,它就可以被重新使用了。所以,缓冲区一旦被读取出来,已经没有使用价值了。
以 Mellow 为例,填充后为 Mellow,但如果我们仅仅想读取 llow。读取完后,缓冲区就可以重新使用了。Me 这两个位置对于我们而言是没用的。我们可以将 llow 复制至 0 - 3 上,Me 则被冲掉。但是 4 和 5 仍然为 o 和 w。这个事我们当然可以自行通过 get 和 put 来完成,但 api 给我们提供了一个 compact() 的函数,此函数比我们自己使用 get 和 put 要高效的多。
Compact 之前的缓冲区
buffer.compact() 会使缓冲区的状态图如下图所示:
Compact 之后的缓冲区
这里发生了几件事:
- 数据元素 2 - 5 被复制到 0 - 3 位置,位置 4 和 5 不受影响,但现在正在或已经超出了当前位置,因此是“死的”。它们可以被之后的 put() 调用重写。
- Position 已经被设为被复制的数据元素的数目,也就是说,缓冲区现在被定位在缓冲区中最后一个“存活”元素的后一个位置。
- 上界属性被设置为容量的值,因此缓冲区可以被再次填满。
- 调用 compact() 的作用是丢弃已经释放的数据,保留未释放的数据,并使缓冲区对重新填充容量准备就绪。该例子中,你当然可以将 Me 之前已经读过,即已经被释放过。
◇ 缓冲区的比较
有时候比较两个缓冲区所包含的数据是很有必要的。所有的缓冲区都提供了一个常规的 equals() 函数用以测试两个缓冲区的是否相等,以及一个 compareTo() 函数用以比较缓冲区。
两个缓冲区被认为相等的充要条件是:
- 两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且buffer 绝不会等于非 buffer对象。
- 两个对象都剩余同样数量(limit - position)的元素。Buffer 的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同。
- 在每个缓冲区中应被 get() 函数返回的剩余数据元素序列([position, limit - 1] 位置对应的元素序列)必须一致。
两个被认为是相等的缓冲区
两个被认为是不相等的缓冲区
缓冲区也支持用 compareTo() 函数以词典顺序进行比较,当然,这是所有的缓冲区实现了 java.lang.Comparable 语义化的接口。这也意味着缓冲区数组可以通过调用 java.util.Arrays.sort() 函数按照它们的内容进行排序。
与 equals() 相似,compareTo() 不允许不同对象间进行比较。但 compareTo()更为严格:如果你传递一个类型错误的对象,它会抛出 ClassCastException 异常,但 equals() 只会返回 false。
比较是针对每个缓冲区你剩余数据(从 position 到 limit)进行的,与它们在 equals() 中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。如果一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。这里有个顺序问题:下面小于零的结果(表达式的值为 true)的含义是 buffer2 < buffer1。切记,这代表的并不是 buffer1 < buffer2。
Java代码
- if (buffer1.compareTo(buffer2) < 0) {
- // do sth, it means buffer2 < buffer1,not buffer1 < buffer2
- doSth();
- }
◇ 批量移动
缓冲区的设计目的就是为了能够高效传输数据,一次移动一个数据元素并不高效。如你在下面的程序清单中所看到的那样,buffer API 提供了向缓冲区你外批量移动数据元素的函数:
Java代码
- 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) 是等价的。
对于以下几种情况的数据复制会发生异常:
- 如果你所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不变,同时抛出BufferUnderflowException异常。
- 如果缓冲区中的数据不够完全填满数组,你会得到一个异常。这意味着如果你想将一个小型缓冲区传入一个大型数组,你需要明确地指定缓冲区中剩余的数据长度。
如果缓冲区存有比数组能容纳的数量更多的数据,你可以重复利用如下代码进行读取:
Java代码
- 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 中而已,这里不再赘述。
◇ 创建缓冲区
Buffer 的七种子类,没有一种能够直接实例化,它们都是抽象类,但是都包含静态工厂方法来创建相应类的新实例。这部分讨论中,将以 CharBuffer 类为例,对于其它六种主要的缓冲区类也是适用的。下面是创建一个缓冲区的关键函数,对所有的缓冲区类通用(要按照需要替换类名):
Java代码
- 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代码
- // 这段代码隐含地从堆空间中分配了一个 char 型数组作为备份存储器来储存 100 个 char 变量。
- CharBuffer charBuffer = CharBuffer.allocate (100);
- /**
- * 这段代码构造了一个新的缓冲区对象,但数据元素会存在于数组中。这意味着通过调用 put() 函数造成的对缓
- * 冲区的改动会直接影响这个数组,而且对这个数组的任何改动也会对这个缓冲区对象可见。
- */
- char [] myArray = new char [100];
- CharBuffer charbuffer = CharBuffer.wrap (myArray);
- /**
- * 带有 offset 和 length 作为参数的 wrap() 函数版本则会构造一个按照你提供的 offset 和 length 参
- * 数值初始化 position 和 limit 的缓冲区。
- *
- * 这个函数并不像你可能认为的那样,创建了一个只占用了一个数组子集的缓冲区。这个缓冲区可以存取这个数组
- * 的全部范围;offset 和 length 参数只是设置了初始的状态。调用 clear() 函数,然后对其进行填充,
- * 直到超过 limit,这将会重写数组中的所有元素。
- *
- * slice() 函数可以提供一个只占用备份数组一部分的缓冲区。
- *
- * 下面的代码创建了一个 position 值为 12,limit 值为 54,容量为 myArray.length 的缓冲区。
- */
- CharBuffer charbuffer = CharBuffer.wrap (myArray, 12, 42);
通过 allocate() 或者 wrap() 函数创建的缓冲区通常都是间接的。间接的缓冲区使用备份数组,你可以通过上面列出的 api 函数获得对这些数组的存取权。
boolean 型函数 hasArray() 告诉你这个缓冲区是否有一个可存取的备份数组。如果这个函数的返回 true,array() 函数会返回这个缓冲区对象所使用的数组存储空间的引用。如果 hasArray() 函数返回 false,不要调用 array() 函数或者 arrayOffset() 函数。如果你这样做了你会得到一个 UnsupportedOperationException 异常。
如果一个缓冲区是只读的,它的备份数组将会是超出 limit 的,即使一个数组对象被提供给 wrap() 函数。调用 array() 函数或 arrayOffset() 会抛出一个 ReadOnlyBufferException 异常以阻止你得到存取权来修改只读缓冲区的内容。如果你通过其它的方式获得了对备份数组的存取权限,对这个数组的修改也会直接影响到这个只读缓冲区。
arrayOffset(),返回缓冲区数据在数组中存储的开始位置的偏移量(从数组头 0 开始计算)。如果你使用了带有三个参数的版本的 wrap() 函数来创建一个缓冲区,对于这个缓冲区,arrayOffset() 会一直返回 0。不理解吗?offset 和 length 只是指示了当前的 position 和 limit,是一个瞬间值,可以通过 clear() 来从 0 重新存数据,所以 arrayOffset() 返回的是 0。当然,如果你切分(slice() 函数)了由一个数组提供存储的缓冲区,得到的缓冲区可能会有一个非 0 的数组偏移量。
◇ 复制缓冲区
缓冲区不限于管理数组中的外部数据,它们也能管理其他缓冲区中的外部数据。当一个管理其他缓冲器所包含的数据元素的缓冲器被创建时,这个缓冲器被称为视图缓冲器。
视图存储器总是通过调用已存在的存储器实例中的函数来创建。使用已存在的存储器实例中的工厂方法意味着视图对象为原始存储器的你部实现细节私有。数据元素可以直接存取,无论它们是存储在数组中还是以一些其他的方式,而不需经过原始缓冲区对象的 get()/put() API。如果原始缓冲区是直接缓冲区,该缓冲区(视图缓冲区)的视图会具有同样的效率优势。
继续以 CharBuffer 为例,但同样的操作可被用于任何基本的缓冲区类型。用于复制缓冲区的 api:
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()
复制一个缓冲区会创建一个新的 Buffer 对象,但并不复制数据。原始缓冲区和副本都会操作同样的数据元素。
duplicate() 函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的 position、limit 和 mark 属性。对一个缓冲区你的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。duplicate() 复制缓冲区:
Java代码
- 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代码
- CharBuffer buffer = CharBuffer.allocate(8);
- buffer.position(3).limit(5);
- CharBuffer sliceBuffer = buffer.slice();
创建分割缓冲区
◇ 字节缓冲区(ByteBuffer)
ByteBuffer 只是 Buffer 的一个子类,但字节缓冲区有字节的独特之处。字节缓冲区跟其他缓冲区类型最明显的不同在于,它可以成为通道所执行的 I/O 的源头或目标,后面你会发现通道只接收 ByteBuffer 作为参数。
字节是操作系统及其 I/O 设备使用的基本数据类型。当在 JVM 和操作系统间传递数据时,将其他的数据类型拆分成构成它们的字节是十分必要的,系统层次的 I/O 面向字节的性质可以在整个缓冲区的设计以及它们互相配合的服务中感受到。同时,操作系统是在内存区域中进行 I/O 操作。这些内存区域,就操作系统方面而言,是相连的字节序列。于是,毫无疑问,只有字节缓冲区有资格参与 I/O 操作。
非字节类型的基本类型,除了布尔型都是由组合在一起的几个字节组成的。那么必然要引出另外一个问题:字节顺序。
多字节数值被存储在内存中的方式一般被称为 endian-ness(字节顺序)。如果数字数值的最高字节 - big end(大端),位于低位地址(即 big end 先写入内存,先写入的内存的地址是低位的,后写入内存的地址是高位的),那么系统就是大端字节顺序。如果最低字节最先保存在内存中,那么系统就是小端字节顺序。在 java.nio 中,字节顺序由 ByteOrder 类封装:
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()。
每个缓冲区类都具有一个能够通过调用 order() 查询的当前字节顺序:
Java代码
- 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 中将会被作为基本数据直接存取。
ByteBuffer 类有所不同:默认字节顺序总是 ByteBuffer.BIG_ENDIAN,无论系统的固有字节顺序是什么。Java 的默认字节顺序是大端字节顺序,这允许类文件等以及串行化的对象可以在任何 JVM 中工作。如果固有硬件字节顺序是小端,这会有性能隐患。在使用固有硬件字节顺序时,将 ByteBuffer 的内容当作其他数据类型存取很可能高效得多。
为什么 ByteBuffer 类需要一个字节顺序?字节不就是字节吗?ByteBuffer 对象像其他基本数据类型一样,具有大量便利的函数用于获取和存放缓冲区内容。这些函数对字节进行编码或解码的方式取决于 ByteBuffer 当前字节顺序的设定。ByteBuffer 的字节顺序可以随时通过调用以 ByteOrder.BIG_ENDIAN 或 ByteOrder.LITTL_ENDIAN 为参数的 order() 函数来改变:
Java代码
- 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 的字节顺序。视图的字节顺序设定在创建后不能被改变,而且如果原始的字节缓冲区的字节顺序在之后被改变,它也不会受到影响。
◇ 直接缓冲区
内核空间(与之相对的是用户空间,如 JVM)是操作系统所在区域,它能与设备控制器(硬件)通讯,控制着用户区域进程(如 JVM)的运行状态。最重要的是,所有的 I/O 都直接(物理内存)或间接(虚拟内存)通过内核空间。
当进程(如 JVM)请求 I/O 操作的时候,它执行一个系统调用将控制权移交给内核。当内核以这种方式被调用,它随即采取任何必要步骤,找到进程所需数据,并把数据传送到用户空间你的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进内存。
I/O 缓冲区操作简图
从图中你可能会觉得,把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?首先,硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色。
因此,操作系统是在内存区域中进行 I/O 操作。这些内存区域,就操作系统方面而言,是相连的字节序列,这也意味着I/O操作的目标内存区域必须是连续的字节序列。在 JVM中,字节数组可能不会在内存中连续存储(因为 JAVA 有 GC 机制),或者无用存储单元(会被垃圾回收)收集可能随时对其进行移动。
出于这个原因,引入了直接缓冲区的概念。直接字节缓冲区通常是 I/O 操作最好的选择。非直接字节缓冲区(即通过 allocate() 或 wrap() 创建的缓冲区)可以被传递给通道,但是这样可能导致性能损耗。通常非直接缓冲不可能成为一个本地 I/O 操作的目标。
如果你向一个通道中传递一个非直接 ByteBuffer 对象用于写入,通道可能会在每次调用中隐含地进行下面的操作:
- 创建一个临时的直接 ByteBuffer 对象。
- 将非直接缓冲区的内容复制到临时直接缓冲区中。
- 使用临时直接缓冲区执行低层 I/O 操作。
- 临时直接缓冲区对象离开作用域,并最终成为被回收的无用数据。
这可能导致缓冲区在每个 I/O 上复制并产生大量对象,而这种事都是我们极力避免的。如果你仅仅为一次使用而创建了一个缓冲区,区别并不是很明显。另一方面,如果你将在一段高性能脚本中重复使用缓冲区,分配直接缓冲区并重新使用它们会使你游刃有余。
直接缓冲区可能比创建非直接缓冲区要花费更高的成本,它使用的内存是通过调用本地操作系统方面的代码分配的,绕过了标准 JVM 堆栈,不受垃圾回收支配,因为它们位于标准 JVM 堆栈之外。
直接 ByteBuffer 是通过调用具有所需容量的 ByteBuffer.allocateDirect() 函数产生的。注意,wrap() 函数所创建的被包装的缓冲区总是非直接的。与直接缓冲区相关的 api:
Java代码
- 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。
◇ 视图缓冲区
I/O 基本上可以归结成组字节数据的四处传递,在进行大数据量的 I/O 操作时,很又可能你会使用各种 ByteBuffer 类去读取文件内容,接收来自网络连接的数据,等等。ByteBuffer 类提供了丰富的 API 来创建视图缓冲区。
视图缓冲区通过已存在的缓冲区对象实例的工厂方法来创建。这种视图对象维护它自己的属性,容量,位置,上界和标记,但是和原来的缓冲区共享数据元素。
每一个工厂方法都在原有的 ByteBuffer 对象上创建一个视图缓冲区。调用其中的任何一个方法都会创建对应的缓冲区类型,这个缓冲区是基础缓冲区的一个切分,由基础缓冲区的位置和上界决定。新的缓冲区的容量是字节缓冲区中存在的元素数量除以视图类型中组成一个数据类型的字节数,在切分中任一个超过上界的元素对于这个视图缓冲区都是不可见的。视图缓冲区的第一个元素从创建它的 ByteBuffer 对象的位置开始(positon() 函数的返回值)。来自 ByteBuffer 创建视图缓冲区的工厂方法:
Java代码
- 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 视图:
Java代码
- /**
- * 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 类为每一种原始数据类型提供了存取的和转化的方法:
Java代码
- 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 的字节数据,就好像一个数据元素被存储在那里一样。根据这个缓冲区的当前的有效的字节顺序,这些字节数据会被排列或打乱成需要的原始数据类型。
如果 getInt() 函数被调用,从当前的位置开始的四个字节会被包装成一个 int 类型的变量然后作为函数的返回值返回。实际的返回值取决于缓冲区的当前的比特排序(byte-order)设置。不同字节顺序取得的值是不同的:
Java代码
- // 大端顺序
- int value = buffer.order(ByteOrder.BIG_ENDIAN).getInt();
- // 小端顺序
- int value = buffer.order(ByteOrder.LITTLE_ENDIAN).getInt();
- // 上述两种方法取得的 int 是不一样的,因此在调用此类方法前,请确保字节顺序是你所期望的
如果你试图获取的原始类型需要比缓冲区中存在的字节数更多的字节,会抛出 BufferUnderflowException。
来自 <http://www.cnblogs.com/lxzh/archive/2013/05/10/3071680.html>