前面翻译过一篇类似的文章,发现讲的不够透彻,这篇是一篇扩展型的文章。
本文注意结合buffer类的API解析buffer内部的机制,并且只介绍读写,其他的请参考buffer的原代码,自己可以进行分析【可能后续也有相关的补充】,本文使用的实现类为ByteBuffer 。
也可以看看我翻译blog了解下buffer的基本东西
http://xianglp.iteye.com/blog/1842063
缓冲区是能原始数据类型的数组包装对象,Buffer类比数组的最大好处为它将数据的信息和数据内容封装到唯一的对象中。Buffer类和它的实现子类定义了处理数据缓冲区的API。下图为buffer类的集成及buffer类的实现。
(图一:buffer类继承图)
属性
每一种Java基本类型的缓冲区都是抽象类Buffer的子类,从Buffer的源代码中可以发现,它定义了四个私有属性。 四个私有属性用于处理缓冲区的数据。buffer类中定义如下:
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
Capacity(容量):
缓冲区能装载的最大数据量,缓冲区被创建时被设置,并不会被更改。
Position(位置):
被读取或被写入的下一个元素的索引,调用get( ) 和 put( )方法时会关联的更新position,换句话说,写入buffer时,Position代表buffer第一个未被写入元素的索引【即put下个元素存放的位置】,读取时,Position代表buffer代表第一个未被读取的索引【即get取时第一个元素位置】。
Limit(限制):
缓冲区中不能被读取或被写入的第一元素。换句话说,在缓冲区中可以使用的元素个数,写入buffer时,Limit为Capacity即能写入buffer最多元素个数,读取buffer是Limit为buffer中元素的个数即写入状态是Position的值。
Mark(标记):
被记录的位置,调用mark( )方法时设置mark=position。调用reset( )设置position = mark,mark属性未被设置时,默认为-1;
四个属性的关系如下:
mark <= position <= limit <= capacity
创建buffer
使用的是实现类中的allocate方法。创建一个容量大小为10的ByteBuffer示例如下:
ByteBuffer readBuffer = ByteBuffer.allocate(10);
前面我们提到的各个属性情况怎么样呢,示例图显示如下:
(图二:创建buffer时存储结构示例图)
创建完毕buffer后可以被写入【即写模式】,position代表未被写入第一元素为0,被写入时limit 等于capacity为10代表缓冲区能写入的byte数,mark为-1(后面有原代码说明)。capacity为固定值10,其他三个属性在buffer被使用的能被修改。
mark属性的看下原代码,在buffer的allocate方法如下
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer的构造方法如下
class HeapByteBuffer
extends ByteBuffer
{
// For speed these fields are actually declared in X-Buffer;
// these declarations are here as documentation
/*
protected final byte[] hb;
protected final int offset;
*/
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
super为 ByteBuffer,super的各个值属性如下,
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
从这里我们可以发现mark的被设置为-1。
上面的源码我们发现buffer实际是数组的包装,实际数据存储对象为一个数组,byteBuffer的构造函数中 new byte[cap],创建的就是一个byte的数组。
Accessing(访问,读写buffer)
API如下:
public abstract class ByteBuffer
extends Buffer implements Comparable
{
// This is a partial API listing
public abstract byte get( );
public abstract byte get (int index);
public abstract ByteBuffer put (byte b);
public abstract ByteBuffer put (int index, byte b);
}
填充:
ByteBuffer在子类HeapByteBuffer有put方法的实现如下
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
//获取写入索引位置
final int nextPutIndex() { // package-private
//写入范围校验,必须为position和limit之间
if (position >= limit)
throw new BufferOverflowException();
//先将position返回,position再自加1
return position++;
}
从代码中我们可以看出put就是数据放置到数组对应的position位置,然后position=position+1指向下个空闲位置。从这里我们也看可以看出put值放入的范围为position位置到limit之间。
示例:
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l')
.put((byte)'o');
各属性值变化如下
(图三:写入buffer时存储结构示例图)
往里进行put时,只有position值发生变化,例如上面的填充了5个字节的大小,position值为5,指上buffer里面的第6个元素。
put (int index, byte b)可以让我们操作buffer中已经操作过位置的值。
buffer.put(0, (byte)'M').put((byte)'w');
变化后的图示如下
(图四:调用 put (int index, byte b)时存储结构示例图)
使用 put (int index, byte b)并不会影响到buffer的相关属性值的变化
Flipping(将读状态转换到写状态)
buffer被填充后,我们怎样将数据读出来呢,flip(),能将buffer由写模式切换至读模式。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
其实flip主要是调整position位置为0,limit设置为position ,mark 设置为-1。
从这里我们可以看出如果执行了flip方法,直接往buffer里面写入值的话,整个buffer从0开始被重新,但是最多只能写入到limit,而如果执行读的话,我们从索引为0的开始读,最多读取limit个数据。
上个buffer执行flip后各属性状态变成为
(图五:flip方法执行后存储结构示例图)
从图可以看出buffer的position位置变为0,代表可以从0索引开始读取数据,limit设置为原来的position位置6,代表我能从索引位置0读取到5。
Draining(从buffer读取)
get方法是从buffer里面读取数据,在子类HeapByteBuffer有put方法的实现如下
public byte get() {
return hb[ix(nextGetIndex())];
}
//注意这里offset值为0
protected int ix(int i) {
return i + offset;
}
//获取position值
final int nextGetIndex() { // package-private
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
可以看出get方法获取的是position与limit之间的数据,每获取后position位置+1,即下个get索引所在位置,所以无论我们调用什么方法操作buffer后,最后使用get方法获取的都是position与limit之间的数据。
调用示例如下
buffer.flip();
System.out.println( (char)buffer.get() );
上面的buffer我们flip后调用get整个存储变成
(图六:调用get方法是buffer的存储示例图)
position指向下个索引位置即为1,其他属性不变化
clear(清空buffer)
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
清空比较明确,即调整position调整为0,limit 调整为capacity,mark调整为-1。我们的上面的示例buffer执行clear后各个属性情况如下
(图七:清空buffer时各属性的变化)
从这里我们可以看出,clear()并未实际的清空数据,而只是调整相关属性。
mark的使用
API如下:
//mark
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
从API中可以发现MARK实际就用来标记当前position位置,reset方法是将position重新设置为mark,这样我们就可以使用mark进行重复读取。但是为调用mark() 函数直接使用reset方法将抛出异常,因为我们的mark属性默认为-1。
完整的示例
package sample;
import java.nio.ByteBuffer;
public class BufferReadWrite {
public static void main( String args[] ){
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l')
.put((byte)'o');
buffer.put(0, (byte)'M').put((byte)'w');
buffer.flip();
//获取能读取的数据个数limit - position
int count = buffer.remaining( );
for (int i = 0; i < count; i++) {
// System.out.println(buffer.limit());
// System.out.println(buffer.capacity());
// System.out.println(buffer.position());
System.out.println("读取前的:"+buffer.toString());
System.out.println( (char)buffer.get() );
System.out.println("读取后的:"+buffer.toString());
}
}
}
结果如下:
读取前的:java.nio.HeapByteBuffer[pos=0 lim=6 cap=10]
M
读取后的:java.nio.HeapByteBuffer[pos=1 lim=6 cap=10]
读取前的:java.nio.HeapByteBuffer[pos=1 lim=6 cap=10]
e
读取后的:java.nio.HeapByteBuffer[pos=2 lim=6 cap=10]
读取前的:java.nio.HeapByteBuffer[pos=2 lim=6 cap=10]
l
读取后的:java.nio.HeapByteBuffer[pos=3 lim=6 cap=10]
读取前的:java.nio.HeapByteBuffer[pos=3 lim=6 cap=10]
l
读取后的:java.nio.HeapByteBuffer[pos=4 lim=6 cap=10]
读取前的:java.nio.HeapByteBuffer[pos=4 lim=6 cap=10]
o
读取后的:java.nio.HeapByteBuffer[pos=5 lim=6 cap=10]
读取前的:java.nio.HeapByteBuffer[pos=5 lim=6 cap=10]
w
读取后的:java.nio.HeapByteBuffer[pos=6 lim=6 cap=10]
其他读写情况分析
(1)我们在写入buffer后,不是用flip方法,再进行get会出现什么情况呢代码如下:
//未使用flip方法
public void noflip( ){
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l')
.put((byte)'o');
buffer.put(0, (byte)'M').put((byte)'w');
int count = buffer.remaining( );
for (int i = 0; i < count; i++) {
// System.out.println(buffer.limit());
// System.out.println(buffer.capacity());
// System.out.println(buffer.position());
System.out.println("读取前的:"+buffer.toString());
System.out.println( (char)buffer.get() );
System.out.println("读取后的:"+buffer.toString());
}
结果输出如下
从图四我们知道,buffer写入后,不执行flip方法,buffer的position=6,limit=10。我们知道buffer的读取是读取position与limit之间的数据,所以我们使用get方法时读取到的是该buffer未填充的数值。
2: put (int index, byte b)中index大于position的位置时,是什么样的情况,是否能读取到。
代码示例如下:
//放入的索引大于position的情况
public void putInedexbig( ){
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l')
.put((byte)'o');
buffer.put(8, (byte)'M');
int count = buffer.remaining( );
buffer.flip();
count = buffer.remaining( );
for (int i = 0; i < count; i++) {
// System.out.println(buffer.limit());
// System.out.println(buffer.capacity());
// System.out.println(buffer.position());
System.out.println("filp后读取前的:"+buffer.toString());
System.out.println( (char)buffer.get() );
System.out.println("filp后读取后的:"+buffer.toString());
}
}
结果我直接说了读取不到, put (int index, byte b)并未影响到position与limit的变化源代码如下:
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;
}
所以flip后position变成了0,limit为原来position即为5,因此put到index为8的记录无法获取。
注意的:
put( byte b )方法却会影响到position的变化,put (int index, byte b)并不影响到任何属性的变化。
get()方法会影响到position的变化, get(int i)不会影响到任何属性的变化。
总结:(1)buffer的实际存储对象为数组,buffer就是对数组操作的封装。
(2)buffer核心是四个属性【position,Limit,Capacity,mark】,相关的操作都是围绕着四个属性展开。
(3)buffer读取核心为position,Limit两个属性,写入时,写入索引开始位置为position,结束位置为Limit。读取是读取开始的索引位置为position,结束索引位置为Limit。
(4)buffer的清空不是实际的清空,而只是相关属性的调整。
(5)buffer的方法是非线程安全的,在高并发环境中注意同步。