JAVA NIO 详解Buffer类
Buffer类基本概念:
一般而言,Buffer的数据结构是一个保存了原始数据的数组,在Java语言里面封装成为一 个带引用的对象。Buffer一般称为缓冲区,该缓冲区的优点在于它虽然是一个简单数组,但是它封装了很多数据常量以及单个对象的相关属性。针对 Buffer而言主要有四个主要的属性:
- 容 量(Capacity ): 容量描述了这个缓冲区最 多能够存放多少,也是Buffer的最大存储元素量,这个值是在创建Buffer的时候指定的,而且不可以更改
- 限 制(Limit ): 不能够进行读写的缓冲区 的第一个元素,换句话说就是这个Buffer里面的活动元素数量
- 位 置(Position ): 下一个需要进行读写的元 素的索引,当Buffer缓冲区调用相对get()和set()方法的时候会自动更新Position的值
- 标记( Mark ): 一个可记忆的 Position位置的值,当调用mark()方法的时候会执行mark = position,一旦调用reset()的时候就执行position = mark,和Position有点不一样,除非进行设置,否则Mark值是不存在的。
按照上边的对应关系可以知道:
0 <= mark <= position <= limit <= capacity
这几个关系可以用下图进行描述:
[1]Buffer的基本操 作:
Buffer管理(Accessing):
一般情况下Buffer可以管理很多元素,但是在程序开发过程中我们 只需要关注里面的活跃 元素 ,如上图小于limit位置的这些元素,因为这些元素是真正在IO读写过程需要的。当Buffer类调用了put()方法的时候,就在 原来的Buffer中插入了某个元素,而调用了get()方法过后就调用该位置的活跃元素,取出来进行读取,而Buffer的get和put方法一直很神 秘,因为它存在一个相对和绝对的概念:
在相对版本 的put 和get 中,Buffer本身不使用index 作为参数,当相对方法调用的时候,直接使用position作为基点,然后运算 调用结果返回,在相对操作的时候,如果position的值过大就有可能抛出异常 信息;同样的相对版本的put方法调用的时候当调用元素超越了limit 的限制的时候也会抛出BufferOverflowException 的 异常 ,一般情况为:position > limit 。
在绝对版本 的put和 get 中,Buffer的position却不会收到影响,直接使用index 进行调用,如果index越界的时候直接抛出 Java里面常见的越界异常 :java.lang. IndexOutOfBoundException 。
针对get和put方法的两种版本的理解可以查阅API看看方法 get的定义【这里查 看的是Buffer类的子类ByteBuffer的API】 :
public abstract byte get() throws BufferUnderflowException
public ByteBuffer get(byte [] dst) throws BufferUnderflowException
public ByteBuffer get(byte [] dst,int offset,int length) throws BufferUnderflowException,IndexOutOfBoundException
public abstract byte get(int index) throws IndexOutOfBoundException
【*:从上边的API详解里面可以知道,Buffer本身支持的两种方式的访问是有原因 的,因为Buffer本身的设计目的是为了使得数据能够更加高效地传输,同样能够在某一个时刻移动某些数据。当使用一个数组作为参数的时候,整个 Buffer里面的 position位置放置了一个记录用的游标,该游标不断地在上一次操作完结的基础 上进行移动来完成Buffer本身的数据的读取,这种情况下一般需要提 供一个length参数,使用该参数的目的就是为了防止越界操作的发生。如果请求的数据没有办法进行传输,当读取的时候没有任何数据能够读取的时候,这个 缓冲区状态就不能更改了,同时这个时候就会抛出BufferUnderflowException的异常 ,所以在向缓冲区请求的时候使用数组结构存储时, 如果没有指定length参数,系统会默认为填充整个数组的长度,这种情况和上边IO部分的缓冲区的设置方法类似。也就是说当编程过程需要将一个 Buffer数据拷贝到某个数组的时候(这里可以指代字节数组),需要显示指定拷贝的长度,否则该数组会填充到满,而且一旦当满足异常 条件:即limit 和position不匹配的时候,就会抛异常 。】
[2]Buffer填充 (Filling):
先看一段填充ByteBuffer的代码: buffer.put((byte )'H' ).put((byte )'e' ).put((byte )'l' ).put((byte )'l' ).put((byte )'o' );
当这些字符传入的时候都是以ASCII值存储的,上述操作的存储步骤图如下:
buffer.put(0,(byte )'M' ).put((byte )'w' );
第一个方法使用了绝对的方式使用index参数替换了缓冲区中的第一个字节,而第二个使用了相对版本的put方 法,所以最终形成的Buffer存储结构图为:
[3]Buffer的反转(Flipping)
当我们在编程过程中填充了一个Buffer过后,就会对该Buffer进行消耗 (Draining) 操作,一般是将该Buffer传入一个通道(Channel) 内 然后输出。但是在Buffer传入到通道中过后,通道会调用get()方法来获取Buffer里面数据,但是Buffer传入的时候是按照顺序传入到通道 里面的,如上边的结构可以知道,本身存储的数据可能为“Mellow” ,但是当通道读取Buffer里面的内容的时候,有可能取到不正确的数据,原因 在于通道读取Buffer里面的数据的时候标记是从右边开始的,这样读取的数据如果从position开始就会有问题 【*:当然不排除有这样一种情况缓 冲区提供了读取策略是双向的,那么这样读取出来的所有的字符就有可能是反向的】 。其实这点概念很容易理解,因为Buffer读取过后会按照 顺序读入到通道(Channel) 中,而通道获取数据的时候会从最右边的position位 置 开始,所以针对这样的情况如果需要正确读取里面的内容就需要对Buffer进行反转操作 ,该操作的手 动代码如下:
buffer.limit(buffer.position()).position(0);
但是因为Java中Buffer类的API提供了类似的操作,只需要下边的方法就可以了:
buffer.flip();
经过flip操作过后,其存储结构就会发生相对应的变化:
[4]Buffer的“消费”(Draining):
当一个Buffer缓冲区填满数据过后,应用程序就会将该缓冲区送入一个通道内进行“消费” , 这种“消 费” 操作实际上使用通道来读取Buffer里面存储的数据,如果需要读取任何一个位置上的元素,则需要先flip操作才 能够顺利接受到该Buffer里面的元素数据,也就是说在Channel通道调用get()方法之前先调用flip()方法 ,【*:这里这种方式的调用是相 对调用过程,从参数可以知道,这里的get()是相对方法的常用调用方式】 在通道“消费” Buffer 的过程中,有可能会使得position达到limit,不过Buffer类有一个判断方法hasRemaining() , 该方法会告诉程序position 是否达到了limit ,因为position一旦超越了limit过后会抛出BufferOverflowException 异 常,所以最好在迭代读取Buffer里面的内容的时候进行判断,同时Buffer类还提供了一个remaining()方法返回目前的limit的值。
——[$] Fill和Drain两个方法代码例子——
package org.susan.java.io;
import java.nio.CharBuffer;
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'",
"Help Me! Help Me!"
};
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 ;
}
public static void main(String args[]) throws Exception{
CharBuffer buffer = CharBuffer.allocate (100);
while (fillBuffer(buffer)){
buffer.flip();
drainBuffer (buffer);
buffer.clear();
}
}
}
该方法的输出如下:
当一个Buffer进行了Fill和Drain操作过后,如果需要重新使用该Buffer,就 可以使用reset()方法,这里reset()就是清空数据并且重置该Buffer,对比上边的Flip()操作的“重置position” 就 很容易理解Buffer的使用过程了。这里列举了很多方法,防止混淆摘录Java API里面的Buffer抽象类的所有方法列表,这是抽象类Buffer里面的所有方法列表,上边介绍的方法若没有在此出现则就应该在它的子类中:
public abstract Object array(): 返回底 层缓冲区的实现数组
public abstract int arrayOffset(): 返回该缓冲区底层实现数 组的偏移量
public int capacity(): 返回该缓冲区的容量
public Buffer clear():清除 该 缓冲区
public Buffer flip():反转 此 缓冲区
public abstract boolean hasArray(): 判断该缓冲区是否有可访问的底 层实现数组
public abstract boolean hasRemaining(): 判断该 缓冲区在当前位置和限 制 之间是否有元素
public abstract boolean isDirect(): 判断该缓冲区是否为直接缓冲区
public abstract boolean isReadOnly(): 判断该缓冲区是否为只读缓冲区
public int limit(): 返回该缓冲区的限制
public Buffer limit(int newLimit): 设置此缓冲区的限制
public Buffer mark(): 在此缓冲区的位置设置标记
public int position(): 返回此缓冲区的位置
public Buffer position(int newPosition): 设置此缓冲区的位置
public int remaining(): 返回当前位置与限制之间的元素数
public Buffer reset(): 将此缓 冲区的位置重置为以前标 记的位置
public Buffer rewind(): 重绕 此缓冲区
[5]Buffer的压缩(Compacting):
很多时候,应用程序有可能只需要从缓冲区中读取某一部分内容而不需要读取所有,而有时候又需要 从原来的位置重新填充,为了能够进行这样的操作,那些不需要读取的数据要从缓冲区中清除掉,这样才能使得第一个读取到的元素的索引变成0,这种情况下就需 要使用compact() 操 作来完成,这个方法从上边Buffer类的列表中可以知道并不包含该方法,但是每个Buffer子类实现里面都包含了这个方法,使用该方法进行所需要的读 取比使用get() 更 加高效,但是这种情况只在于读取部分缓冲区内的内容。这里分析一个简单的例子:
当上边这样的情况使用了 buffer.compact() 操作后,情况会演变成下边这种样子:
标记方法mark()使得该Buffer能够记住某个位置并且让position在返回的时候不用返回初始索 引0而直接返回标记处进行操作 ,若mark没有定义,调用reset()方法将会抛出InvalidMarkException 的 异常 ,需要注意的是不要混淆reset()方法和clear()方法,clear()方法单纯清空该Buffer里面的元素,而reset()方法在清空基础 上还会重新设置一个Buffer的四个对应的属性 ,其实Marking很好理解,提供一段代码和对应的图示:
buffer.position(2).mark().position(4);
当上边的buffer调用了方法reset过后:
如上边所讲,position最终回到了mark处而不是索引为0的 位置
[7]Buffer的比较 (Comparing):
在很多场景,有必要针对两个缓冲区进行比较操作,所有的Buffer 都提供了equals()方法用来比较两个Buffer,而且提供了compareTo()方法进行比较。既然是两个Buffer进行比较,它们的比较条 件为:
- 两 个对象应该是 同类型 的,Buffer包含了 不同的数据类型就绝对不可能相等
- 两 个Buffer对象position到limit之间的元素数量(remaining返回值)相同,两个Buffer的 容量可以不一样 ,而且两个Buffer的 索引位置也可以不一样 ,但是Buffer的remaining(从 position到limit)方法返回值必须是相同的
- 从remaining段的出示位置到结束位置里面的每一个元素都 必 须相 同
两个相同Buffer图示为(equals()返回true ):
两个不相同的Buffer图示为(equals() 返回为false ):