一、前言
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的子类中使用,如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 的不足之处。