Java NIO学习篇之缓冲区Buffer详解

定义

缓冲区Buffer在java nio中负责数据的存储,缓冲区就是数组,用于存储不同类型数据的数组。

jdk为java七大基本类型数据都准备了响应的缓冲区(boolean值除外):

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntegerBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

上述缓冲区除了ByteBuffer的 功能稍微多点外,因为ByteBuffer是通用的,所以功能会比较多。其他6种的使用方式几乎是一致的。

/*
* Buffer以上七大基本类型缓冲区类的基类,是个抽象类,定义了一些通用方法以及一些抽象方法。
* 具体方法都是final修饰的,不可被子类覆盖。
*
public abstract class Buffer {

    /**
     * 不知道啥用
     */
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;

    //标记当前的position 位置,用reset方法的话,可以把position设置为mark值位置。
	//如果mark=-1的话会reset失败。
    private int mark = -1;
    //游标,标记缓冲区下一个要读取或者写入的数组下标。
    private int position = 0;
    //表示缓冲区数组可以操作(读取、写入)的可用数据长度。
    private int limit;
    //缓冲区容量
    private int capacity;

	//以上四个属性符合 mark<=position<=limit<=capacity,不符合就会报错。

    // 只对直接缓冲区有用
    // 记录直接缓冲区的内存地址,用于提高JNI 的方法GetDirectBufferAddress的效率。
    //ByteBuffer才支持直接缓冲区,所以到后面关于ByteBuffer的会讲。
    long address;

    // 传入mark、position、limit、capacity值创建一个缓冲区的构造器
    //是个包内私有的构造器
    Buffer(int mark, int pos, int lim, int cap) { 
         //缓冲区容量小于0时抛出异常。
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);

		//根据传参设置缓冲区大小。
        this.capacity = cap;
        //设置limit
        limit(lim);
        //设置position
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                //mark大于pos并且mark大于等于0时会抛出异常
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            //设置mark
            this.mark = mark;
        }
    }

    /**
     * 返回缓冲区容量
     */
    public final int capacity() {
        return capacity;
    }

    /**
     * 返回缓冲区下一次要读取或者写入的数组下标
     */
    public final int position() {
        return position;
    }

    /**
     * 设置缓冲区的游标
     */
    public final Buffer position(int newPosition) {
        //要设置的position值不能大于limit值或者小于0,否则就抛出异常。
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        //设置游标
        position = newPosition;
        //如果设置的新的游标值比mark值小,就重置mark值为-1.
        if (mark > position) mark = -1;
        return this;
    }

    /**
     *返回缓冲区的可用数据长度。
     */
    public final int limit() {
        return limit;
    }

    /**
     * 设置缓冲区的limit值,同时会保证position和mark值保证这几个值的关系符合规则。
     */
    public final Buffer limit(int newLimit) {
    	//对要设置的新limit进行检查,如果大于缓冲区容量或者小于0就抛出异常
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        //设置limit值
        limit = newLimit;
        //如果游标比可操作的最大数组下标还大的话,就把游标设置为limitif (position > limit) position = limit;
        //如果mark > limit,重置mark。
        if (mark > limit) mark = -1;
        return this;
    }

    /**
     * 标记当前position位置。
     */
    public final Buffer mark() {
        mark = position;
        return this;
    }

    /**
     * 把游标恢复到标记的位置
     */
    public final Buffer reset() {
        int m = mark;
        //标记小于0时抛出异常
        if (m < 0)
            throw new InvalidMarkException();
        //重置游标至标记处
        position = m;
        return this;
    }

    /**
     * 清空缓冲区,其实就设置游标为0,limit设置回capacity值,mark重置,数据还是存在的。
     */
    public final Buffer clear() {
    	
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

    /**
     * 缓冲区创建时默认是写模式的,这个方法把缓冲区改为读模式。
     * 每次通过通道往存储设备中写数据都需要调用此方法把缓冲区设置为读模式。读取缓冲区的数据。
     * 最后详细讲解这个
     */
    public final Buffer flip() {
        //把limit设置为position
        limit = position;
        //将游标设置为0
        position = 0;
        //重置mark。
        mark = -1;
        return this;
    }

    /**
     * 重置游标,从新开始读、写数据。
     */
    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

    /**
     * 读模式下,返回剩余可读的数据长度,写模式下,返回剩余可写的缓冲区长度。
     */
    public final int remaining() {
        return limit - position;
    }

    /**
     * 返回是否还有数据可读或者可写。
     */
    public final boolean hasRemaining() {
        return position < limit;
    }

    /**
     * 返回缓冲区是否只读,抽象方法,具体看实现类。
     */
    public abstract boolean isReadOnly();

    /**
   
    /**
     * 是否直接缓冲区,true为直接缓冲区
     */
    public abstract boolean isDirect();


    //////////////////分割线,上面是公共方法API,下面是包私有方法,我们不能调用的//////

    /**
     * 读模式下游标往右移一位,也就是跳过一个数据。
     * 返回移动前的游标值。
     */
    final int nextGetIndex() {                          // package-private
    	//因为游标加一 所以要确保游标加一前要小于等于limit
        if (position >= limit)
        	//当前游标>=limit抛出异常
            throw new BufferUnderflowException();
        return position++;
    }

	/**
	* 读模式下游标往右移动n位。
	* 返回移动前的游标
	*/
    final int nextGetIndex(int nb) {                    // package-private
        if (limit - position < nb)
        	//判断加n后的游标是否大于limit,大于就抛出异常
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        //返回移动前的游标。
        return p;
    }

    /**
     * 写模式下游标往右移动1位。
	 * 返回移动前的游标
	 * 逻辑与上面的一样。
     */
    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }

     /**
     * 写模式下游标往右移动n位。
	 * 返回移动前的游标
	 * 逻辑与上面的一样。
     *
    final int nextPutIndex(int nb) {                    // package-private
        if (limit - position < nb)
            throw new BufferOverflowException();
        int p = position;
        position += nb;
        return p;
    }

    

    final int markValue() {                             // package-private
    	//返回标记的值
        return mark;
    }

    //把缓冲区置为不可用
    final void truncate() {                             // package-private
    	
    	//重置mark为-1
        mark = -1;
        //啥都置零
        position = 0;
        limit = 0;
        capacity = 0;
    }

    final void discardMark() {                          // package-private
        //重置游标-1
        mark = -1;
    }

    
}

limit、position与缓冲区的读写模式:

Buffer有分读模式和写模式,其实质是由limit值和position值决定的。这种模式没有特定的死规定。

写模式:
往缓冲区Buffer里面填充数据的模式。Buffer刚刚创建好时就是处于这个模式。

public class ByteBufferDemo {

    public static void main(String[] args) {
    	//创建一个10和字节大小的字节缓冲区。
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        System.out.println("capacity = " + byteBuffer.capacity());
        System.out.println("limit = " + byteBuffer.limit());
        System.out.println("position = " + byteBuffer.position());
    }
}

结果:
在这里插入图片描述
相应的图:
在这里插入图片描述
解释:
此时的游标position=0,limit=10,所以可以通过put方法(此方法在各自子类中申明定义,基类没有申明)往Buffer中写入n个数据。position也会相应右移n为,但是n必须符合n<=limit-position才行。否则会抛出异常。

写入5个数据后:

public class ByteBufferDemo {

    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        //写入5个数据。
        byteBuffer.put("hello".getBytes());
        System.out.println("capacity = " + byteBuffer.capacity());
        System.out.println("limit = " + byteBuffer.limit());
        System.out.println("position = " + byteBuffer.position());
    }
}

执行结果:
在这里插入图片描述
相应的图:
在这里插入图片描述
解释:
因为写入了5个数据,所以position=5,继续写入数据会从缓冲区数组的下标5开始写入,此时剩余的可写数据长度为limit-position=5。

此时,数据写入完成,用通道channel读取缓冲区数据到设备上,通道channel读取缓冲区数据到设备上会把缓冲区的数组从position值开始读,一直读到limit前结束。

假设我们没有把缓冲区从写模式切换到读模式,通道channel读取缓冲区数据时就会从position=5的下标读取到limit=10前的9下标。也就是读取缓冲区数组的5-9下标,这些下标的元素是没有被写入的,所以是错误的。

public class ByteBufferDemo {

    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        byteBuffer.put("hello".getBytes());
        byte[] bytes = new byte[byteBuffer.remaining()];
        //读取缓冲区,这里就不往设备上读了
        byteBuffer.get(bytes);
        System.out.println(Arrays.toString(bytes));
    }
}

执行结果:
在这里插入图片描述
解释:
读取的是后面的没有被写入的缓冲区数组区域,是错误的。

所以要把缓冲区切换为读模式:
读模式:从缓冲区数组中读取缓冲区数组数据的模式。


public class ByteBufferDemo {

    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        byteBuffer.put("hello".getBytes());
        //切换为读模式
        byteBuffer.flip();
        System.out.println("capacity = " + byteBuffer.capacity());
        System.out.println("limit = " + byteBuffer.limit());
        System.out.println("position = " + byteBuffer.position());
    }
}

//切换为读模式前,limit=10 position=5
//从上面的flip方法的源码可知,该方法把limit值设置为position值,把position设置为0.
//所以调用flip方法后应该是limit=5 position=0.

执行结果:
在这里插入图片描述
相应图:
在这里插入图片描述
此时处于读模式,如果读取缓冲区的数组从position读取到limit前一个下标,就会读取下标0-4的数据,这样就是正确的读取。所以每次进行数据读取时都要把缓冲区切换为读模式才能正确地读取数据。

public class ByteBufferDemo {

    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        byteBuffer.put("hello".getBytes());
        byteBuffer.flip();
        byte[] bytes = new byte[byteBuffer.remaining()];
        byteBuffer.get(bytes);
        System.out.println(Arrays.toString(bytes));
        System.out.println("==============>读取数据后");
        System.out.println("capacity = " + byteBuffer.capacity());
        System.out.println("limit = " + byteBuffer.limit());
        System.out.println("position = " + byteBuffer.position());

        //读完后要情况缓冲区,重写开始写数据
        System.out.println("==============>清空缓冲区后");
        byteBuffer.clear();
        System.out.println("capacity = " + byteBuffer.capacity());
        System.out.println("limit = " + byteBuffer.limit());
        System.out.println("position = " + byteBuffer.position());
    }
}


执行结果:
在这里插入图片描述
解释:

  1. 读到的数据不为0,正确地读到了数据。
  2. 读取数据后,缓冲区将变得不可用,因为limit-position=0,不能写也不能继续读。
  3. 需要把缓冲区清空后恢复到写模式来继续写数据。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章