Java NIO的Buffer通常是用来和Channel作交互的。如你所知的,数据的读取是通过channel到buffer的,而数据的写入,是从buffer到channel的。
Buffer的本质是内存中的一个可读可写的数据块,这个数据块由NIO的Buffer对象封装,提供了一系列的支持内存操作的函数。
Buffer的基础用法
使用buffer,读写数据一般来说都有以下四步:
- 把数据写入到Buffer
- 调用flip函数
- 从Buffer中读取数据
- 调用clear函数或者compact函数
当你向buffer中写入数据时,buffer会持续追踪已经写入数据的数量。一旦你需要读取数据,就需要调用flip函数把buffer从writing模式转化成reading模式。在reading模式下,可以从buffer中读取所有已写入的数据。
一旦所有数据读取完毕,你需要清空buffer,以便buffer可以准备下一次的写入。清空可以使用调用clear方法或compact方法实现。clear方法会把整个buffer的数据清空,compact只会清空那些已经读取过的数据。那些未读取的数据会被移动到buffer的头部,后续的新数据会接在未读数据后面写入。
下面是一个简单的Buffer例子:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
Buffer Capacity,Position 和 Limit
Buffer有三个重要属性,你需要非常了解,以便清楚它是如何工作的。属性分别是:
- capacity
- position
- limit
position和limit的意义取决于当前的buffer是reading模式还是writing模式。capacity的意义是不变的,不受其他参数影响。
下面是一个关于capacity、position、limit的图:
Capacity
作为一个内存块,一个Buffer必须要有一个固定的大小,这个属性也被叫做“capacity”。你只能把primitive类型数据写入buffer中。一旦buffer满了,你就需要把它清空(把数据读出来或者直接清空数据),否则不能再写入任何数据。
Position
当向buffer中写数据时,你是在一个特定的位置写入。默认position是0.当有一个primitive数据写入,position会自动的指向下一个存储空间(数组的索引会自增)。position属性达到最大后会变成 -1。
当从buffer中读数据时,你也是从一个指定的position开始操作。当调用flip函数将writing模式转化为reading模式时,position会被重置成0。随着从buffer中读取数据,position会自增。
Limit
在writing模式下,limit表示你有多少内存块可以供你写入,此时limit和capacity是等价的。
当调用flip切换为reading模式后,limit表示你可以读取的最大索引(position已经重置为0)。换句话说,就是把你已经写入的数据都读出来。
Buffer Types
Java NIO 自带如下几种:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
如你所见,这些buffer都只支持primitive类型数据。
Allocating a buffer
要获得一个Buffer对象,你必须先给它分配大小。每个Buffer类都有一个allocate函数来做这件事。下面这个例子就是分配了48个bytes的capacity的ByteBuffer。
ByteBuffer buf = ByteBuffer.allocate(48);
下面是一个分配了1024个char的CharBuffer
CharBuffer buf = CharBuffer.allocate(1024);
Writing Data to a Buffer
有两种方式可以写数据:
- 从Channel写入到Buffer
- 直接调用Buffer的put方法写入
int bytesRead = inChannel.read(buf); //read into buffer.
buf.put(127);
put方法有许多重载,允许以各种方式写入,详情参考JavaDoc。
flip()
flip方法用来切换buffer的writing模式到reading模式。调用flip方法把position设置为0,把limit设置成之前position的位置(因为随着数据的写入position会移动)。
换言之,在reading模式下,position标识了数据读取的开始位置,limit标识了结束位置。
从Buffer中读数据
有两种方法可以读数据:
- 通过channel读取buffer数据
- 调用buffer自己的get方法
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();
get方法和之前的put方法一样,有很多方法重载。
rewind()
Buffer的rewind方法会把position设置为0,因此你可以重复读取buffer中的数据。limit属性是不可达的,因此会把buffer修改成可读所有数据的状态。
clear() 和 compact()
一旦数据读取完毕,为了使buffer可以继续等待写入,需要调用clear方法或compact方法。
如果调用了clear方法,position会置回0,limit会变成和capacity一样。换句话说,buffer被清空了,但是buffer中的数据是没有消失的,仅仅是标记了你可以写入buffer的起始位置和终止位置。
如果还有一些未读数据,你调用了clear方法,就会失去数据现有的位置信息。
如果有数据未读,而且你还想稍后读取,但是现在需要写入一些数据,那么就使用compact方法代替clear方法。
compact方法会把未读数据复制到buffer的头部,然后设置position为未读数据的尾部索引,limit仍然是capacity。这样一来buffer就可以继续写入了,而且不会覆盖那些未读的数据。
mark() 和 reset()
通过mark函数,可以在buffer的指定位置进行标记。之后可以通过reset方法将position修改为mark的位置。例如:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals() 和 compareTo()
equals()
判断两个buffer是否相等:
- 它们必须是同类型的(byte、char、int etc.)
- 它们现有的部分(可能是调用了compact之后,索引会发生变动)数量是相同的
- 所有现有数据相同
equals方法只会比较buffer中的一部分数据,不是buffer中的每个都要比较。事实上,他只比较剩余的buffer数据。
compareTo()
使用了排序程序进行比较,在以下情况会认为一个比另一个“小”:
- 一个buffer的位置上的值比另一个buffer同位置的值小
- 所有元素都一样,但是第一个buffer比第二个buffer先耗光(它的元素更少)