写点什么

Java NIO 关键概念之 Buffer

作者:老农小江
  • 2022-11-02
    广东
  • 本文字数:6016 字

    阅读完需:约 20 分钟

一、前言

Java NIO 的三大关键概念之一是 Buffer,在一些文章/源代码中,我们也经常会看到 Buffer 相关的信息。Buffer 到底是什么,Buffer 的基本使用方法是什么,这是本文主要要说的。

二、Buffer 的基本概念

Buffer自 JDK1.4 引入,是一个抽象类,在java.nio包下,定义了一些通用的方法,并不能直接创建对象,在使用时,需要通过其具体的基础数据类型子类来创建对象。Java 的基础数据类型中,除了boolean外,都有一个对应数据类型的子类,如 ByteBuffer、IntBuffer 等。



通俗点说,Buffer 是 Java 基础数据类型的数据容器,本质上其实就是一个相应基础数据类型的数组封装,并扩展了相关的属性、操作方法等以方便使用。(实现缓存数据、方便高效地操作数据,后面会有示例/源码说明)

Buffer 中,有几个重要的属性/概念,简单说明如下:

简单来说,capacity 是 Buffer 对应的数组的容量值,limit 是读/写的限制值,position 是数组中相关元素位置的索引值,这三个值都不会为负数,且这几个值的大小关系如下:

mark <= position <= limit <= capacity
复制代码

Buffer 作为一个数据容器,操作一个 Buffer 的一般过程如下:



java.nio.Buffer类作为一个抽象基类,提供了一些基本方法,如capacity()limit()等,可以返回其私有属性值,也提供了flip()(读/写模式切换)、clear()(清空数据)等设置属性值的操作方法,且这些方法都是用final关键字修饰的,不可被重写

而要创建一个 Buffer 对象,则只能通过对应基础数据类型的子类中的allocate()方法来实现,同样,写数据、读数据,也需要调用相应子类中的相关put()get()方法来实现。

三、Buffer 的基本使用 &源码分析

基本使用

首先,通过一个例子演示下 Buffer 的基本使用:

import java.nio.ByteBuffer;/** * Java Buffer demo *  * 输出的byteBuffer对象中,可以看到读/写等相关方法操作后,position、limit、capacity值的的变化。 * * @author : laonong */public class ByteBufferMain {    public static void main(String[] args) {        //创建总容量为10的Buffer对象        ByteBuffer byteBuffer = ByteBuffer.allocate(10);        System.out.println("创建Buffer后:" + byteBuffer);        //写入4个节点元素数据        byteBuffer.put((byte) 'a');        byteBuffer.put((byte) 'b');        byteBuffer.put((byte) 'c');        byteBuffer.put((byte) 'd');        System.out.println("写入数据后:" + byteBuffer);        //读写转换        byteBuffer.flip();        System.out.println("flip()方法读写转换后:" + byteBuffer);        //读取Buffer中的所有元素数据        System.out.print("读取Buffer中的数据:");        while (byteBuffer.hasRemaining()) {            System.out.print((char)byteBuffer.get());            if (byteBuffer.hasRemaining()) {                System.out.print(",");            }        }        System.out.println();        System.out.println("读取数据后:" + byteBuffer);        //重置Buffer        byteBuffer.clear();        System.out.println("clear()方法重置后:" + byteBuffer);    }}
复制代码

执行代码输出:

创建Buffer后:java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]写入数据后:java.nio.HeapByteBuffer[pos=4 lim=10 cap=10]flip()方法读写转换后:java.nio.HeapByteBuffer[pos=0 lim=4 cap=10]读取Buffer中的数据:a,b,c,d读取数据后:java.nio.HeapByteBuffer[pos=4 lim=4 cap=10]clear()方法重置后:java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
复制代码

从输出结果可以看出,随着对 Buffer 进行各种读/写操作,Buffer 中的三个关键属性中,position、limit 的值在不断变化,capacity 的值固定不变,如下图所示:


源码分析

接下来,我们通过源码,看一下 Buffer 到底是如何实现示例中的相关功能的。

示例代码中,几个关键类的关系如下图所示:



1、java.nio.Buffer部分源码:

package java.nio;import java.util.Spliterator;/** * A container for data of a specific primitive type. */public abstract class Buffer {      //标记当前的position    private int mark = -1;    //读/写位置游标    private int position = 0;    //buffer读/写的上限边界值    private int limit;    //buffer的最大容量值    private int capacity;      //构造函数    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;        }    }     /**    * 清除数据     *(可以看出,并不是真正意义上的删除buffer中的元素,而是重置属性的值[数据的索引值])    */    public final Buffer clear() {        position = 0;        limit = capacity;        mark = -1;        return this;    }    /**     * 翻转buffer       * (通过重置属性值的方式,实现切换buffer读/写模式的目的。【注意:只能一次从写模式翻转到读模式,不能反复翻转】)     */    public final Buffer flip() {        limit = position;        position = 0;        mark = -1;        return this;    }    /**     * 倒带buffer     *  (用于当buffer已经读过一遍了,但还需要从头读一次,则需要调用一次该方法)     */    public final Buffer rewind() {        position = 0;        mark = -1;        return this;    }      /**     * 判断position、limit之间是否还有元素 ()     */    public final boolean hasRemaining() {        return position < limit;    }      /**     * position值自增 (用于子类中写数据时调用)     */   final int nextPutIndex() {                          // package-private        if (position >= limit)            throw new BufferOverflowException();        return position++;    }     /**     * position值自增 (用于子类中读数据时调用)     */   final int nextGetIndex() {                          // package-private        if (position >= limit)            throw new BufferUnderflowException();        return position++;    }}
复制代码

2、java.nio.ByteBuffer 部分源码:(其它 IntBuffer、CharBuffer 等 Buffer 子类源码相似)

package java.nio;/** * A byte buffer. */public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {    /**     * 内部属性     *(这几个属性主要是在ByteBuffer的子类中使用,如HeapByteBuffer)     */    final byte[] hb;                  // Non-null only for heap buffers    final int offset;    boolean isReadOnly;                 // Valid only for heap buffers      /**     * 分配(创建)一个新的指定容量的 ByteBuffer     */    public static ByteBuffer allocate(int capacity) {        if (capacity < 0)            throw new IllegalArgumentException();        return new HeapByteBuffer(capacity, capacity);    }      /**     * 读数据      * (读取的是数组当前索引值为position的数据,读取后,position的值加1)     *     */    public abstract byte get();    /**     * 写数据      * (写数据的位置当前索引值为position,写数据后,position的值加1)     */    public abstract ByteBuffer put(byte b);      /**     * 压缩buffer     */    public abstract ByteBuffer compact();}
复制代码

java.nio.ByteBuffer 类中的get()put(byte b)compact()这几个方法都只有抽象定义,具体实现在子类中,接下来看下HeapByteBuffer中的具体实现代码。

3、java.nio.ByteBuffer 部分源码:

package java.nio;/** * A read/write HeapByteBuffer. */class HeapByteBuffer extends ByteBuffer {     /**  * 添加偏移的索引值  */  protected int ix(int i) {    return i + offset;  }   /**  * 写数据   */  public ByteBuffer put(byte x) {        hb[ix(nextPutIndex())] = x;        return this;  }   /**  * 读数据   */  public byte get() {    return hb[ix(nextGetIndex())];  }    /**  * 压缩buffer  * (复制数组中未读的元素,然后通过设置position、limit的值,将数据移动到buffer的头部,  *  这样的话,buffer继续写数据就不会覆盖未读的数据)  */  public IntBuffer compact() {        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());        position(remaining());        limit(capacity());        discardMark();        return this;    }}
复制代码

以上,笔者只贴了部分关键代码,代码中已添加简单说明。更多详细代码可以去看源码,源码中有非常详细的相关注释。

源码补充说明:

1、对 Buffer 的清除、压缩、翻转等操作,在某种意义上来说,其实就是对数据索引值 position、limit 的操作

2、Buffer 中的清除数据(clear()方法),并不会真正删除 Buffer 中数组中的元素,而是通过设置 position、limit 属性值的方式,修改 Buffer 的读/写位置与边界值。

3、如果 Buffer 中的数据没有全部读完,就调用clear()方法的话,则可能会造成未读数据被覆盖掉,此时,如果想只清除已读数据,保留未读数据的话,可调用compact()方法。

4、Buffer 通过优秀的设计实现了可以方便灵活、简单高效地缓存/操作数据。


Buffer 的常用方法简单说明如下

当然,buffer 的子类中还有许多其它方法,用于更灵活地操作 buffer,如put(int index, byte b);方法可指定索引值写数据,get(int index);方法可指定索引值读取数据等等,此处笔者就不一一列举了。


Buffer 的大小比较

另外,需要说明的是,Buffer 的具体基础数据类型子类都实现了Comparable接口,并重写了equals()compareTo()方法, 也就是说,Buffer 是可以比较大小的,只是需满足特定条件:

ByteBuffer 关键源码如下:

  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;  }  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);  }
复制代码

从源码可知:

1、判断两个 buffer 相等需满足以下条件:

a、有相同的元素类型;(同为 byte、int 等)

b、buffer 中剩余元素的个数相等;

c、buffer 中剩余元素的值都一一对应相等。

2、判断两个 buffer 大小根据以下条件判断:

a、有相同的元素类型;(同为 byte、int 等)

b、剩余第一个不相等元素的大小;(两个 buffer 中,从当前 position 开始依次比较;当出现第一个不相等的元素时,以该元素的大小比较结果,作为 buffer 大小的比较结果)

c、剩余元素前面比较都相等,更长的那个大。(若依次比较 buffer 中的剩余元素后,其中一个 buffer 的剩余元素已经全部比较完,另一个 buffer 还存在元素没有参与比较,则还存在元素的 buffer 大。)

从以上条件可以看出,判断 buffer 是否相等、buffer 的大小,只会校验 buffer 中的“剩余”元素,并不会校验全部元素,这点需要注意。(剩余元素指从 position 到 limit 之间的元素,也就是 limit - position的差值)


四、小结

1、Buffer 缓冲区本质上是一个基础数据类型的数组,但又不仅仅是一个数组,它提供了高效地对数据的结构化访问,以及跟踪数据元素的读/写位置。

2、对 Buffer 的读/写切换、清除、压缩等操作,在某种意义上来说,其实就是对数据元素索引值 position、limit 的操作。

3、Buffer 并没有提供直接删除数据元素的方法,而是只能覆盖。所以,当调用 clear() / compact() 方法后,只是重新设置了 position、limit、mark 的值,原来的数据还是在 Buffer 中的,在覆盖之前依然是可以读取到的。

4、Buffer 写满数据后,依然是可以继续写入的,只是需要设置写入的位置,并且写入新数据后原数据会被覆盖;同样,Buffer 中的数据是可以重复读取的,只要调用 rewind() 方法即可。(或者可指定索引位置读/写数据)

5、Buffer 可以比较大小,且比较时只会校验比较剩余元素。

6、Buffer 分配的最大容量创建时就固定了,不支持动态扩/缩容,Buffer 的读/写模式切换也较为不便等等(flip() 方法调用麻烦且易出错),这些也可以说是 Buffer 的不足之处。



越学越无知,我是老农小江,欢迎交流~


发布于: 刚刚阅读数: 5
用户头像

老农小江

关注

好好学习,天天向上 2020-03-26 加入

越学越无知,一个普通老码农,欢迎交流~

评论

发布
暂无评论
Java NIO关键概念之Buffer_Java_老农小江_InfoQ写作社区