蜜蜂花园

导航

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值存储的,上述操作的存储步骤图如下:
  这里需要留意一点就是填充的时候需要进行强制转换 , 因为Java里面所有的字符格式都是16bit的Unicode格式 ,而上边代码里面填充的时候使用的参数是字符的,如果不进行强制转换会出现数据丢失 的情 况,ASCII码表示字符的时候使用的是8位数据,而Unicode方式保存字符本身和ASCII保存有很大的区别,为了不出现乱码保证存储到缓冲区字符 的正确性,一定记住需要进行强制换转,转换成为对应的字节方式保存。再继续针对该Buffer进行填充操作:
buffer.put(0,(byte )'M' ).put((byte )'w' );
  第一个方法使用了绝对的方式使用index参数替换了缓冲区中的第一个字节,而第二个使用了相对版本的put方 法,所以最终形成的Buffer存储结构图为:
  从上边两个操作可以知道,Buffer类在填充该Buffer的时候使用相对方法 和绝对方法有很大的区别,上图可以看到原来存入的“Hello” 现在变成了“Mellow” ,这就是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操作过后,其存储结构就会发生相对应的变化:
  【*:这个地方仔细想想,经过了Flip操作过后,从逻辑意义上讲,确实 Buffer被反转了,因为这个时候通道类(Channel)读取Buffer的时候会从position地方继续读取,不会出现读取异常 的情况。与其说 是 “反 转” ,不如说是“重置” ,只是这里的“重置” 不 会清空缓冲区里面的数据,仅仅是 将缓冲区的limit属性和position属性进行重设 ,和真正调用reset方法的 时候还是存在一定区别的,至于这里Flip翻译称为“反转” 我不做说明,只要读者能够理解上边的步骤而且 知道这是一次Flip操作就可以了,这里正确理解的是“重置position” 我们在编程中也经常看见 rewind()方法,该方法和flip()类似,但是该方法不会影响到limit的变化,它仅仅会将position设置为0,所以可以直接使用 rewind方法进行 “重新读取” 还需要说明一点是如果进行了两次flip()操作 的话, 第二次操作会同时将 position和limit设置为0 ,这样的话如果再进行基于缓冲区的相对读取过程就会BufferOverflowException
  [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的值。
  *:Buffer并不是线程 安全的,如果需要多线程 操作一个Buffer,需要自己定义同步来操作 Buffer, 提供一个相关例子:
  ——[$] 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();
        }
    }
}
  该方法的输出如下:
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!
  【*:这段输出其实看不出来什么问题 ,但是NIO 的效率明显胜过IO,这个是可以通过一些测试 的例 子来证明的。】
  当一个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() 操作后,情况会演变成下边这种样子:
  【*:仔细分析上边的内容,究竟发生了什么事情呢?上边这一段将可以读取到的 “llow” 拷贝到了索引0-3的位 置,而4和5成为了不可读的部分,但是继续移动position仍然可以读取到但是它们这些元素已经“死亡” 了, 当调用put()方法的时候,它们就会被 覆盖 掉,而且limit设置到 了容量位置,则该Buffer就可以重新被完全填充。当Buffer调用了compact方法过后将会放弃已经消费过的元素,而且使得该Buffer可以 重新填充, 这种方式类似一个先进先出的队列(FIFO) ,可以这样理解,compact()方法将position和limit之间的 数据复制到开始位置,从而为后续的put()/read()让出空间,position的值设置为要赋值的数组的长度,limit值为容量,这里摘录一段 网上讲解的compact方法的使用场景: 如果有一个缓冲区需要写数据,write()方法的非阻塞调用只会写出其能够发送的数据而不会阻塞等待所有的数据都发 送完成,因此write()方法不一定会将缓冲区中所有的元素都发出去,又假设现在需要调用read()方法,在缓冲区中没有发送的数据后面读入新数据, 处理方法就是设置 position = limit 和 limit = capacity ,当然在读入新数据后,再次调用 write()方法前还需要将这些值还原,这样做就会使得缓冲区最终耗尽,这就是该方法需要解决的主要问题
  [6]Buffer的标记(Marking):
  标记方法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 ):

posted on 2010-12-27 09:51  蜜蜂花园  阅读(928)  评论(0编辑  收藏  举报