Java 7之异步I/O第3篇 - 异步I/O操作之Buffer

       在传统的I/O操作中,许多都是将数据直接写入或者将数据直接读到流中,但是在新NIO中,所有数据都是用缓冲区处理的。在之前已经多次接触过缓冲区,而且大多数的类都使用字节数组来做缓冲区。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

       NIO.2的一个新特性就是异步能力。NIO的新功能会涉及到三个重要的类,Channel(通信管道)、Buffer(缓冲)和非阻塞式输出/输出的Selector类,还需要了解Charset类,可以实现字节序列与字符序列的转换。NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们共同解决传统的I/O类中的一些问题。

      这一章将主要讲解一下Buffer的实现,新NIO中提供的主要Buffer类及他们之间的相互关系如下:



对于每个非布尔原始数据类型都有一个缓冲区类,即ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和 ShortBuffer。那么为什么不提供BooleanBuffer呢?

尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。


1、Buffer类


在Buffer类中定义了四个非常重要的属性,如下:

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

详细介绍如下:


(1)容量(capacity)


缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。


(2)上界(limit)


缓冲区的第一个不能被读或写的元素。缓冲创建时,limit 的值等于 capacity 的值。假设 capacity = 1024,我们在程序中设置了 limit = 512,说明Buffer 的容量为 1024,但是从 512 之后既不能读也不能写,因此可以理解成,Buffer 的实际可用大小为 512。


(3)位置(position)


当写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。


(4)标记(mark)


一个备忘位置。标记在设定前是未定义的(undefined)。在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)。

    在使用 Buffer 时,我们实际操作的就是这四个属性的值。

这些属性是私有的,在子类中没有再重复定义,而是通过一些方法来操作。在初始化时调用Buffer类中的包权限构造函数,如下:

   Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: (" + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
    public final Buffer position(int newPosition) {
    	// 由于limit限定可读或可写的末尾边界,新的读写位置不可以大于
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        // mark<=position才是合法的
        if (mark > position) mark = -1;
        return this;
    }
    public final Buffer limit(int newLimit) {
    	// 不允许末尾有效边界大于容量
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
    }
在子类中都是通过super()来调用如上构造函数的。Buffer类中实现的方法都是针对这4个属性进行操作的,并且加入了final关键字,表示不可以被覆盖。

另外许多方法都返回了Buffer对象,这样就可以进行级联的操作。


效果图如下:


如上需要强制类型转换为byte字符,因为Java默认的为Unicode编码占用两个字节。由于兼容ASCII编码,所以省略前八位后值仍然准确。

    // 写模式改变为读模式
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
    // 不设置上界
    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
调用flip()方法的结果如下:


Buffer类中还定义了一些抽象方法,这些抽象方法将在后面进行具体的讲解。


2、ByteBuffer类


子类的实现以ByteBuffer为例,其它子类的实现都非常相似,在这里不再一一讲解。

类中最主要的属性和构造方法如下:

    final byte[] hb;                    // Non-null only for heap buffers
    final int offset;
    boolean isReadOnly;                 // Valid only for heap buffers

    ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset) {
        super(mark, pos, lim, cap);    // 调用父类的构造函数来初始化4个重要属性
        this.hb = hb;
        this.offset = offset;
    }

下面开始介绍一些常用的方法。


1、allocate() 创建缓冲区


要想获得一个Buffer对象首先要指定容量的大小。 每一个Buffer类都有一个allocate()静态工厂方法,源代码如下:

   public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity); // 分配一个HeapByteBuffer实例
    }

得到一个HeapByteBuffer类的实例,构造函数如下:

    HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);
    }
也就是调用父类ByteBuffer的构造函数来为各个属性赋值,其中hb字节数组被赋值new byte[cap],而这个数组分配在Java堆上,是由虚拟机来管理的内存区域,还有一个直接分配内存的方法,如下:

    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
使用直接字节缓冲区可能要比在JVM堆上分配缓冲区的效率更高一些。


2、get()和put() 存取元素


Buffer 类并没有包括 get() 或 put()方法。但是,每一个Buffer 的子类都有这两个方法,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以它们不能在顶层 Buffer 类中被抽象地声明。它们的定义必须被特定类型的子类所遵从。下面以 ByteBuffer 为例讲解get()和put()方法了。 

    public abstract byte get();
    public abstract ByteBuffer put(byte b);

定义了无index的 get ()和 put()方法,称为相对存取,相对存取会自动影响缓冲区的位置属性

其在HeapByteBuffer中的具体实现如下:

    protected int ix(int i) {
        return i + offset;
    }
    public byte get() {
        return hb[ix(nextGetIndex())];
    }
    public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x;
        return this;
    }
  
其中的hb就是ByteBuffer中定义的字节数组,offset是定义的偏移量,这里为0,表示不需要偏移。由于NIO偏向于字节存取,所以如果要从一个ByteBuffer中获取整数,则需要offset的值为4.

final int nextGetIndex() {                          // package-private
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}
final int nextPutIndex() {                          // package-private
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}

position 的值会自动前进。如果调用多次导致位置超出上界limit,则会抛出 BufferOverflowException 异常;这种不带索引参数的方法,

    public abstract byte get(int index);
    public abstract ByteBuffer put(int index, byte b);

带索引参数的方法,称为绝对存取,绝对存储不会影响缓冲区的位置属性。以put实现为例:

  public ByteBuffer put(int i, byte x) {
        hb[ix(checkIndex(i))] = x;
        return this;
    }
    final int checkIndex(int i) {                       // package-private
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }
只会检查索引是否合法,并不会改变位置属性的值。


3、compact() 压缩元素


以ByteBuffer为例来看一下HeadByteBuffer类是如何实现这个方法的,源代码如下:

    public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());  // 对元素进行批量移动
        position(remaining()); // 设置下一个要写入的位置
        limit(capacity());     // limit重置为capacity
        discardMark();         // mark标识置为无效
        return this;           // 返回这个ByteBuffer实例
    }

操作属性的方法一般在Buffer中实现,如下:

    public final int remaining() {  // 有效元素的数量
        return limit - position;
    }
    final void discardMark() {     // mark重置为无效                      
        mark = -1;
    }

压缩前:                                                                                                           压缩后

    



4、equals()和compareTo()方法


可以使用equals()和compareTo()方法比较两个Buffer,但是这是有区别的,equals()方法

两个Buffer相等的充要条件是:

  1. 有相同的类型(byte、char、int等)
  2. Buffer中剩余的byte、char等的个数相等
  3. Buffer中所有剩余的byte、char等都相同

equals()只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。

   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;
    }
    private static boolean equals(byte x, byte y) {
        return x == y;
    }
属性不同的缓冲区可以相同


属性不同,所表示的有效区域不同,但是数组内容相同,则被认为是不相等的,如下:



compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:

  1. 第一个不相等的元素小于另一个Buffer中对应的元素 
  2. 所有元素都相等,但第一个Buffer元素个数比另一个少
   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;
        }
        return this.remaining() - that.remaining();  
    }

    private static int compare(byte x, byte y) {
        return Byte.compare(x, y);

    }

Byte中的compare()方法源代码如下:

    public static int compare(byte x, byte y) {
        return x - y;
    }

如果想要更详细的了解equals() 和compareTo(),请参看:

传送门 : Java 7之基础 - 实现比较  http://blog.csdn.net/mazhimazh/article/details/20038281


3、CharBuffer类


1、创建缓冲区

 public static CharBuffer allocate(int capacity) {  // 静态工厂方法
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapCharBuffer(capacity, capacity);
    }
通过分配来创建一个新的缓冲区,与ByteBuffer的创建过程类似。不过还可以使用自己提供的数组来储存缓冲区中的数据元素,如下:

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


与创建的缓冲区相关的方法还有hasArray()和arrayOffset()方法,首先来看hasArray()方法,如下:

   public final boolean hasArray() {
        return (hb != null) && !isReadOnly;
    }
如果缓存区数组不为null且是可读写的,则返回值为true,这样就可以调用arrayOffset()方法了,如下:

   public final int arrayOffset() {
        if (hb == null)
            throw new UnsupportedOperationException();
        if (isReadOnly)
            throw new ReadOnlyBufferException();
        return offset;
    }
返回offset值,表示缓存区数据在数组中存储的开始位置的偏移量。
举一个例子:

       char[] myArray=new char[100]; // 自定义一个数组做为存储区
       // positon=10,limit=20,capacity=myArray.length=100
       CharBuffer cb=CharBuffer.wrap(myArray,10,20);
       
       if(cb.hasArray()){//  true,表示是缓冲区有可存取的备份元素
    	   System.out.println(cb.arrayOffset()); // 默认是数组的0位置
       }












参考主要文献:

Java NIO  Ron  HitChens 著  斐小星 译










你可能感兴趣的:(Java 7之异步I/O第3篇 - 异步I/O操作之Buffer)