Java NIO 之 Buffer

来源:互联网 发布:金英杰免费网络课2017 编辑:程序博客网 时间:2024/05/17 23:15

《Java源码分析》:Java NIO 之 Buffer

在Java API文档中,对Buffer的说明摘入如下:

Buffer:是一个用于特定基本数据类型的容器。这里的特定基本数据类型指的是:除boolean类型的其他基本上数据类型。

缓冲区是特定基本数据类型元素的线性有限序列。除内容外,缓冲区饿基本属性还包括三个重要的属性,如下:

1、capacity(容量):表示Buffer容量的大小。

2、limit(限制):是第一个不应该读取或写入的元素的索引。缓冲区的限制不能为负数,并且不能大于其容量。

3、position(位置):表示下一个要读取或写入的元素的索引。缓冲区的位置不能为负数,并且不能大于其限制。

刚看文档的时候,可能对limit和position的含义不是太能理解。这个我们看完源码之后,就会懂了哈,不急。

不过在看源码之前,可以提前来通过画图的方式来解释下limit 和 position。

在Buffer中有两种模式,一种是写模式,一种是读模式。

有了上图应该比较好理解position和limit的含义了。

下面就看下Buffer的源代码了。由于Buffer有很多子类,这里主要以IntBuffer为例。

Buffer的几个重要属性

    // Invariants: mark <= position <= limit <= capacity    private int mark = -1;    private int position = 0;//位置    private int limit;//限制    private int capacity;//容量
  • 1
  • 2
  • 3
  • 4
  • 5

这几个属性就也就是我们上面说到的position、limit、capacity,最后还有一个mark。

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;        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

构造函数中的逻辑相当简单,就是一个初始化,不过在里面包括的相关的有效性检查。

要注意的是此构造函数是包私有的。

可能有人像我一样,会有这样一个疑问:Buffer类是抽象类,为什么会有构造函数呢?

由于关于抽象类中有构造函数,自己也是第一次见到。因此查阅的相关资料。发现如下:

1、抽象类是可以有构造函数的。但很多人认为,构造函数用于实例化一个对象(或建立一个对象的实例),而抽象类不能被实例化,所以抽象类不应该有公共的构造函数。但不应该有“公共”的构造函数,和不应该有构造函数,这是两个不同的概念,所以,如果抽象类需要构造函数,那么应该声明为“protected”。

2、既然抽象类是可以,甚至有时候应该有构造函数,那抽象类的构造函数的作用是什么?我觉得至少有两个:

(1)初始化抽象类的成员;(2)为继承自它的子类使用。在Buffer这个抽象类中我们可以明显的感受到以上两点的作用。

3、即使我们声明一个没有构造函数的抽象类,编译器还会为我们生成一个默认的保护级别的构造函数。子类实例化时(不管是否为带参构造)只会调用所有父类的无参构造函数,而带参构造必须通过显式去调用.调用顺序是先调用抽象类的无参构造函数。如果子类实例化时是使用带餐的构造函数,则再接着调用抽象类的带参构造函数,最后调用子类本身的构造函数。

在构造函数,涉及到两个函数:limit(int newLimit)和position(int newPosition),如下:

    //函数的功能:设置Buffer的limit,如果position大于newLimit,则将position设置为新的limit。    //如果mark被定义且大于新的limit,则就会被抛弃。    public final Buffer limit(int newLimit) {        //有效性检查,即limit必须满足这样的关系:0<=limit<=position.        if ((newLimit > capacity) || (newLimit < 0))             throw new IllegalArgumentException();        limit = newLimit;        //如果position大于newLimit,则将position设置为新的limit。        if (position > limit) position = limit;        //如果mark被定义且大于新的limit,则会被抛弃(即设置为-1)        if (mark > limit) mark = -1;        return this;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

limit(int newLimit)此函数的功能:简单来说就是设置limit。

    //函数功能:设置Buffer的position.如果mark被定义且大于new position,则就会被抛弃。    public final Buffer position(int newPosition) {        //有效性检查,即0<=newPosition<=limit.        if ((newPosition > limit) || (newPosition < 0))            throw new IllegalArgumentException();        position = newPosition;        //如果mark被定义且大于new position,则就会被抛弃。        if (mark > position) mark = -1;        return this;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

position(int newPosition)此函数的功能简单来说:就是设置position.

这两个函数都比较简单哈,没有任何的难点。

Buffer中常见的方法介绍

在上篇博文的Demo代码中,我们会看到使用了Buffer中如下几个函数:

1、allocate :分配一段空间的Buffer对象

2、put :用于往Buffer中添加元素

3、flip():用于将写模式转化为读模式

4、hasRemaining:判断Buffer中是否还有元素可读

5、get():读取Buffer中position位置的元素

6、clear() 或者是 compact()方法:清除元素

下面我们主要就看下以上几个方法的源代码

1、allocate(int cap)方法介绍

由于Buffer类是一个抽象类,是不可以实例化对象的,因此在Buffer中是不存在allocate(int cap)方法的,allocate(int cap)方法在其子类中均有实现。这里就以IntBuffer为例,看下Buffer子类IntBuffer的allocate(int cap)方法。

    public static IntBuffer allocate(int capacity) {        if (capacity < 0)            throw new IllegalArgumentException();        return new HeapIntBuffer(capacity, capacity);    }
  • 1
  • 2
  • 3
  • 4
  • 5

函数功能:分配一个新的用来装载int类型数据的Buffer对象实例。这个new buffer的position为0,limit为capacity,mark为未定义的(即为-1)。buffer中的元素全部初始化为零。

在allocate方法中直接是实例化了一个IntBuffer子类的对象。

既然这里涉及要HeapIntBuffer类,我们就看下这个类

    public abstract class IntBuffer        extends Buffer        implements Comparable<IntBuffer>    class HeapIntBuffer        extends IntBuffer  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

从继承关系我们知道:HeapIntBuffer这个类是IntBuffer的子类

这个类的构造函数为

    HeapIntBuffer(int cap, int lim) {            // package-private        super(-1, 0, lim, cap, new int[cap], 0);    }
  • 1
  • 2
  • 3
  • 4
  • 5

在这个构造函数中直接调用了父类IntBuffer中对应的构造函数。看下IntBuffer类中的这个构造函数。

在看构造函数之前,这里要说下IntBuffer类中的几个属性。

    final int[] hb;                  // Non-null only for heap buffers    final int offset;    boolean isReadOnly;                 // Valid only for heap buffers
  • 1
  • 2
  • 3

IntBuffer类中主要包括一个int类型的数组,即从这里我们知道,Buffer类的底层数据结构是借助于数组来完成的。

继续看IntBuffer类的构造方法

    // Creates a new buffer with the given mark, position, limit, capacity,    // backing array, and array offset    //    IntBuffer(int mark, int pos, int lim, int cap,   // package-private                 int[] hb, int offset)    {        super(mark, pos, lim, cap);//调用父类Buffer中对应有参的构造函数        this.hb = hb;        this.offset = offset;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

以上,就是当我们使用如下代码得到IntBuffer实例的整个过程。 
IntBuffer buffer = IntBuffer.allocate(cap);

2、put(int i)方法介绍

下面来看IntBuffer类中的put方法

    public abstract IntBuffer put(int i);    public abstract int get(int index);
  • 1
  • 2
  • 3

在IntBuffer类中put、get方法都是抽象的。 
有了上面allocate方法的过程分析,我们知道IntBuffer buffer = IntBuffer.allocate(cap) 
中的buffer实际上是父类的引用指向的是子类的对象。当我们使用buffer.put(value)/buffer.get(). 
实际上是调用子类HeapIntBuffer类中的put/get方法,这就是多态。在Java中相当重要的一个特征。

HeapIntBuffer类中put方法的实现如下:

    public IntBuffer put(int x) {        hb[ix(nextPutIndex())] = x;        return this;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

put方法实现的思想就是:将值存储在position位置即可。

这里涉及到两个新的函数,分别为:

1、nextPutIndex(),函数功能:简单来说就是返回下一个要写入元素的索引位置(即当前position值),并将position进行加一操作。

2、ix(int i):偏移offset个位置

这两个函数的实现如下:

    //函数功能:首先检查当前的position是否小于limit,如果小于则存储,否则抛异常。    final int nextPutIndex() {                          // package-private        if (position >= limit)            throw new BufferOverflowException();        return position++;    }    //函数功能:偏移offset个位置    protected int ix(int i) {          return i + offset;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3、get()方法介绍

看完了put方法,接下来来看下get方法

函数的功能:取得Buffer中position位置所指向的元素。

    //函数功能:获取buffer中position所指向的元素。    public int get() {        return hb[ix(nextGetIndex())];    }    //函数功能:获取buffer中索引为i位置的元素    public int get(int i) {        return hb[ix(checkIndex(i))];    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在get()方法中也涉及到两个另外的函数:

1、nextGetIndex(),函数功能:返回下一个读取的元素的索引位置(即position值)

2、ix(int i)

    final int nextGetIndex() {                          // package-private        if (position >= limit)            throw new BufferUnderflowException();        return position++;    }    //将position向右移动nb个位置,这个函数目前还没有看见在哪里得到的应用    final int nextGetIndex(int nb) {                    // package-private        if (limit - position < nb)            throw new BufferUnderflowException();        int p = position;        position += nb;        return p;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

以上就是get方法的内部实现,也相当简单哈。

4、flip()方法介绍

其实,在刚开始看别人博客的时候,是最不能理解这个函数的功能的,疑问在于:为什么要在读Buffer中的内容时,要先调用这个方法呢。

其实在自己把Buffer的读模式和写模式弄清楚之后,这个函数的功能我也就明白了。

    /**     * Flips this buffer.  The limit is set to the current position and then     * the position is set to zero.  If the mark is defined then it is     * discarded.     *     * <p> After a sequence of channel-read or <i>put</i> operations, invoke     * this method to prepare for a sequence of channel-write or relative     * <i>get</i> operations.  For example:     *     * <blockquote><pre>     * buf.put(magic);    // Prepend header     * in.read(buf);      // Read data into rest of buffer     * buf.flip();        // Flip buffer     * out.write(buf);    // Write header + data to channel</pre></blockquote>     *     */     //函数功能:将Buffer从写模式转化为读模式。     //具体为:将limit设置为此时position并且将position设置为零。如果mark被定义了则被抛弃(即设置为-1)     //这个方法的应用场景为:在管道读或调用put方法之后,调用此方法来为管道写或者是get操作做准备。    public final Buffer flip() {        limit = position;        position = 0;        mark = -1;        return this;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

以上就是flip()方法的介绍,是不是也比较简单哈。

5、hasRemaining()介绍

函数功能:判断Buffer中是否还有可读取的元素。

    /**     * Tells whether there are any elements between the current position and     * the limit.     */    public final boolean hasRemaining() {        return position < limit;    }   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

此方法的内部实现直接是判断position是否小于limit. 
所以,逻辑也是相当简单的。

6、clear() compact()方法的介绍

clear()函数功能:清空Buffer中所有的元素。

其内部实现直接是将Buffer里面几个属性进行了重置。

    public final Buffer clear() {        position = 0;        limit = capacity;        mark = -1;        return this;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后看下compact方法

compact()方法的功能:将已读元素进行清除,未读元素拷贝保留并拷贝到Buffer最开始位置。这个也是和clear()方法不同的地方。

具体实现如下:

    public IntBuffer compact() {        //先进行拷贝,即将剩余的没有访问的元素拷贝到Buffer从零开始的位置        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());//remaining()函数返回的是剩余元素个数        //设置position为下一个写入元素的位置        position(remaining());        //设置limit为Buffer容量        limit(capacity());        discardMark();//废弃mark,即将mark设置为-1        return this;    }    public final int remaining() {        return limit - position;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

小结

以上就将Buffer的内部实现给过了一篇,过了一篇之后的感受就是其内部实现逻辑是相当相当的简单的哈。

我们只需要理解了Buffer中几种重要的属性:position、limit、capacity在读模式和写模式的含义的区别就可以很好的理解Buffer的内部实现了。

原创粉丝点击