欢迎浏览我的博客 获得更多精彩文章
https://boyn.top
Java NIO 教程 (三) 缓冲区
NIO缓冲区用于跟Channels进行互动.正如上篇文章所说到的,数据从缓冲区读出到通道,从通道写入至缓冲区
缓冲区实际上是一块可以读写的内存区.内存区被缓存区对象封装起来,并且提供了一些可以让我们更加方便操作这块内存区的方法
缓冲区基本用法
用缓冲区来读写数据基本上分为以下4步:
- 向缓冲区中写入数据
- 调用flip()方法
- 从缓冲区中读出数据
- 调用clear()或者compact()方法
当你向缓冲区中写入数据后,缓冲区对象会记录你向其中写入了多少数据.当你需要读取数据时,你需要把缓冲区从写模式切换到读模式(通过调用flip()方法).在读模式中,缓冲区可以令你读取所有写在缓冲区中的数据.(译者注:切换模式在实现上其实只是向两个状态变量赋值,所以切换模式的开销很小,速度快)
当你读取了所有的数据后,你需要清理缓冲区,使我们能够再次向其写入数据.你可以用两种方式达到同样的效果:调用clear()方法,或者调用compact()方法.clear()方法清除整个缓冲区(将整个缓冲区中的数据置为初始值,时间为O(n)),或者调用compact()方法,它只会清除你已经读取的数据,所有未读取的数据将会移至缓冲区的开头,数据会在未读取的数据之后开始向缓冲区写入
以下是一个简单的缓冲区使用的例子,在其中我们用到了write,flip,read和clear方法
public class BasicFileChannelTest {
public static void main(String[] args) throws Exception{
RandomAccessFile aFile = new RandomAccessFile("data\\nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
}
}
缓冲区的Capacity,Position与Limit状态变量
正如前文所言,缓冲区实际上是一块能够写入数据,并且在稍后能够读取数据的区域.这块区域被NIO所实现的缓冲区对象包裹着,实现了封装,并且缓冲区对象还提供了一系列方法来更方便地操作内存块
为了更好的理解缓冲区是如何工作的,缓冲区有三个属性是需要熟悉的
- capacity
- position
- limit
position,limit的意义跟缓冲区在读模式还是在写模式有关,而capacity则永远保持不变,与模式无关
以下这幅图是缓冲区分别在写模式和读模式下,这三个状态变量的位置
[外链图片转存失败(img-CHSMIpPa-1564142637104)(https://cdn.sinaimg.cn.52ecy.cn/large/005BYqpgly1g3owmmzefzj30ej09cwem.jpg)]
Capacity
作为一个内存块,缓冲区是有其确定且固定的大小的,这个大小就被称之为capacity(容量).当缓冲区满了以后,在你想要继续将数据写入缓冲区之前,你需要清空它(读取数据,或者直接清空).
Position
当你把数据写入缓冲区时,这个操作会在某个position(位置)进行.初始化时,position为0,当数据被写入到缓冲区后,position会增加,并指向下一个插入数据的位置.position的最大值为capacity-1(译者注:数组从零开始计数,所以一开始position是指向第一个数据被插入的位置,也就是0,当缓冲区满了之后,position就会指向数组的末端)
当你从缓冲区读取数据时,一样会从某个position开始.当你对缓冲区对象执行了flip()方法后,position会被重置为0.当你读取数据时,position也会随之增加.
Limit
在写模式中,缓冲区的limit属性是你能够向缓冲区写入多少数据,一般limit的值与capacity相同
当执行了flip()方法后,缓冲区进入读模式,此时limit意味着你能从缓冲区中读出多少数据.因此,当你切换到读模式时,limit会被设置成在写模式时position的位置.换句话说,你写入多少,就可以读多少.
例子
我们来看一个实际的例子,以便更好地理解缓冲区三个状态变量的概念
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("程序被初始化,此时三个变量的值分别为:");
System.out.println("position: " + buf.position());
System.out.println("limit: " + buf.limit());
System.out.println("capacity: " + buf.capacity());
buf.put(new Byte("1"));
buf.put(new Byte("2"));
buf.put(new Byte("3"));
System.out.println("读入了三个字节,此时三个变量的值分别为:");
System.out.println("position: " + buf.position());
System.out.println("limit: " + buf.limit());
System.out.println("capacity: " + buf.capacity());
buf.flip();
System.out.println("执行了flip()方法,此时三个变量的值分别为:");
System.out.println("position: " + buf.position());
System.out.println("limit: " + buf.limit());
System.out.println("capacity: " + buf.capacity());
while (buf.hasRemaining()) {
buf.get();
}
buf.clear();
System.out.println("执行了clear()方法,此时三个变量的值分别为:");
System.out.println("position: " + buf.position());
System.out.println("limit: " + buf.limit());
System.out.println("capacity: " + buf.capacity());
执行结果如下:
程序被初始化,此时三个变量的值分别为:
position: 0
limit: 1024
capacity: 1024
读入了三个字节,此时三个变量的值分别为:
position: 3
limit: 1024
capacity: 1024
执行了flip()方法,此时三个变量的值分别为:
position: 0
limit: 3
capacity: 1024
执行了clear()方法,此时三个变量的值分别为:
position: 0
limit: 1024
capacity: 1024
缓冲区类型
NIO定义了如下几个缓冲区类型
- ByteBUffer
- MapperByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
正如你所见,这些缓冲区类型对应不同的基本数据类型,换句话说,你可以将这些基本类型当做缓冲区中的字节进行使用,
不过MapperByteBuffer有一些特殊,会在本系列其他文章中介绍
创建一个缓冲区并为其分配内存
要想获得一个缓冲区对象,你首先需要动态分配它.每个缓冲区类都有allocate()方法来做这个工作.
下面这个例子动态分配了一个容量为48byte的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
下面这个例子动态分配了一个容量为1024byte的CharBuffer
CharBuffer buf = CharBuffer.allocate(1024);
向缓冲区中写入数据
我们有两种方式向缓冲区中写入数据
- 从通道向缓冲区写入数据
- 执行缓冲区对象的put方法
以下是一个从通道向缓冲区写入数据的例子
int bytesRead = inChannel.read(buf); //read into buffer.
以下是一个执行缓冲区对象的put方法的例子
buf.put(127);
关于put()方法,还有许多重载版本,让我们能够以多种方式向缓冲区中写入数据.
具体的方法可以观看官方的文档(此处用的是JDK11)
flip()方法
flip()方法将一个缓冲区对象从写模式切换到读模式.调用flip()方法会使position设置为0(你没有忘记上面所说的三个变量对吧?),并将limit设置为position之前所在的位置.
换句话说,position现在标记读数据开始的位置,limit标记有多少数据写入了缓冲区中(有多少数据能够被读取)
从缓冲区中读出数据
我们有两种方式从缓冲区中读取数据
- 使通道从缓冲区读取数据
- 执行缓冲区对象的get()方法
以下是一个通道从缓冲区读取数据的例子
int bytesWritten = inChannel.write(buf);
以下是一个执行缓冲区对象的get()方法的例子
byte aByte = buf.get();
关于get()方法,还有许多重载版本,让我们能够以多种方式从缓冲区中读取数据.
具体的方法可以观看官方的文档(此处用的是JDK11)
rewind()
rewind方法将position设置为0,让我们可以从重新读取所有缓冲区中的数据.limit则保持不变,因此仍然标记着有多少数据能够从缓冲区中读取
clear()和compact()
当你完全地从缓冲区中读取数据后,你需要让缓冲区能够重新写入数据.clear()和compact()方法都可以完成这个操作
如果你调用了clear()方法,position会被设置成0,limit被设置为capacity.换句话说,缓冲区被重置了,不过缓冲区中的数据没有被清除,只是三个状态变量变为初始化的状态
如果你在完全读取数据之前调用了clear()方法,数据就会被"忘记",也就是说这些数据已经不可被读取(但仍然存在)
如果有上面所说到的情况,那么你需要先写入一些数据(当然不能太多以致未读取的数据被覆盖),然后调用compact()方法
compact()方法将所有未读取的数据复制到缓冲区的头部,然后将position设置为未读取数据的后面,limit仍然设置为capacity.现在缓冲区就可以写入了,并且不会覆盖没有读取的数据
mark()和reset()
你可以为缓冲区标记现在的位置,只需要调用mark()方法.当你读取了一些数据,或者写入了一些数据后,你可以调用reset()方法来把position设置为标记的地方
以下是一个例子
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals()和compareTo()
可以通过这两个方法来比较两个缓冲区
equals()
当满足以下全部条件时,equals方法返回true
- 类型相同(如:同为ByteBuffer)
- 缓冲区中有相同数量的数据
- 这些数据全部相等
如你所见,equals()方法只会比较buffer中的部分属性而非全部属性.事实上,该方法只会比较缓冲区中的数据
compareTo()
compareTo()方法与equals相同,比较的是缓冲区中的数据.当满足下面条件时,缓冲区A会认为比缓冲区B小
- 缓冲区A中数据比缓冲区B中对应位置数据要小(根据字典顺序)