Java NIO学习与记录(二):FileChannel与Buffer用法与说明
上一篇简单介绍了
NIO
,这一篇将介绍FileChannel
结合Buffer
的用法,主要介绍Buffer
一、FileChannel例子
上一篇说到,这个Channel
属于文件通道,专门读取文件信息,NIO读取文件内容的简单的例子:
1 | public static void readFile() { |
rua.txt文件内容为:123456789 上述代码运行后输出如下:
1 | 123456789 |
文件正常读取,可以结合上面的注释,来分析下过程,接下来要利用上面的例子介绍Buffer
的一些概念。
二、Buffer的概念
2.1:Buffer操作的步骤
第一篇说过,Buffer
是一个缓冲区,是一个容器,负责从通道读取数据或者写数据给通道,通过上面的例子,我们可以看到Buffer
在读取通道数据时的几个步骤:
step1:分配空间
1 | ByteBuffer.allocate(1024); //除此之外,还可以通过allocateDirector分配空间(具体不了解,先放一边,回头补) |
step2:从通道读取数据,写入Buffer
1 | int bytesRead = fileChannel.read(buf); |
step3:读取Buffer
内的内容,切换回Buffer
读模式
1 | buf.flip(); //切换到Buffer读模式,读模式下可以读取到之前写入Buffer的数据 |
step4:循环执行读写操作,每取完一次Buffer
中的值,都切换回写模式,再次从通道读取数据到Buffer
1 | while (bytesRead != -1) { //当读不到任何东西时返回-1 |
2.2:Buffer读写流程详解
2.2.1:重要属性的介绍
Buffer
具备的几个重要概念:
capacity
:缓冲区数组的总长度(容积)position
:下一个需要操作的数据元素的位置limit
:缓冲区不可操作的下一个元素的位置,limit <= capacity
mark
:用于记录position
的前一个位置或默认是-1
2.2.2:Buffer内部的操作流程
结合上面的概念,除了mark
(再往下介绍),其余几个指标的操作变化如下图:
初始化一个容积为10的Buffer
,初识位置limit = capacity
,position
位于第一个位置
当容器内写入了5个数据元素之后,position
的位置变到了第6个位置,标记当前写入到哪里了
这时候不再写入数据了,开始切换回Buffer
的读模式(flip),发生的变化如下:
发现,原先读模式下的position
的位置被limit
替换掉了,而position
被重置为了第一个位置,这是因为现在读模式下想要读取之前写入的内容,为了保证读取的数据都是可读的(之前写入的),就需要有一个标记,来记录之前写模式下,操作到哪里了,position
被重置为第一个位置,也很容易理解,因为切换了读模式,从头开始读取已写入的数据。
通过上面的描述,我们清楚了buffer
是如何利用position
、limit
、capacity
来完成读写操作的,下面我们来介绍下具体读写操作时Bufer
发生的操作:
Buffer切换读模式的方法有:flip
Buffer切换写模式的方法有:clear、compact
Buffer读数据:get
使用clear
切换回写模式
的时候,position
会被置为0(也就是最初的位置),limit
置为capacity
(也就是最后的位置),意味着切换写模式之前未读的数据,将会被新一轮的写入覆盖,就再也找不回来了,所以除clear
这个操作,Buffer还提供了compact
方法来切换读模式
,这个方法会把所有未读的数据拷贝到Buffer的起始位置,然后position
指向最后一个未读数据的后一位,这样,下次开启读模式的时候,position
操作同上,置为0,因此之前未读完被落下的数据也就在这时候被读到了。
下面我们来还原下这个转换过程:
走到上面的步骤后,我们切换成写模式,下面这个图分别表示了clear
和compact
两个方法下的两种操作:
根据图4
和图5
,结合上面的话,更容易理解clear
和compact
两种方式切换写模式所做的内部操作,以及为什么clear
会丢数据,而compact
不会。
下面通过一开始的例子,把中间读取数据的地方稍微做下修改:
1 | ByteBuffer buf = ByteBuffer.allocate(2); |
输出结果:
1 | 13579-----------程序结束 |
发现丢了一些数据,现在把clear
改成compact
,运行结果为:
1 | 12345678-----------程序结束 |
发现,除了9,都输出来了(至于9为啥没输出,因为每次只取了一个字节的数据呀~)
那么,如果切换到读模式
,但是不读,然后切换回写模式
继续写,会发生什么?
改造上述代码如下:
1 | ByteBuffer buf = ByteBuffer.allocate(2); |
调用clear的情况下输出:
1 | -----------程序结束 |
而调用compact
方法,却发生阻塞(死循环)了,结合之前的图,我们可以知道,如果不读,意味着在compact
下会把未读的数据copy
到Buffer里,如果一点都不读,那么意味着被copy
的这批数据会占满整个Buffer,以至于position
没有下一个位置可用,就会发生文件里的数据没办法被安排进缓冲区(意味着文件读不完),bytesRead
一直不等于-1,发生死循环。
通过上面的图和例子,基本上可以理清楚读模式
、写模式
(包含不同的切换方式)下的Buffer内部处理方式。
2.3:Buffer的其他操作
除了上面几种常规用法,Buffer还提供了其他的几个操作方法
2.3.1:rewind
这个方法可以在读模式下,重置position的位置
,也就是说在get执行后。position
发生了位移,这个方法可以重置position
的位置为初始位置,看例子:
1 | ByteBuffer buf = ByteBuffer.allocate(2); //每次可读入两个字节 |
运行结果:
1 | 1133557799 |
可以看到,每次循环拿到两个字节,但两次获取的数据都是同一个,因为rewind
把读取游标重置
成初始位置了(也即是位置0)
⭐️ 这里说下,如果把上面的第二个
get方法
去掉,然后把clear模式
改成compact模式
同样也会发生死循环,因为rewind
重置了游标,重置后又没有get方法
再次读取,导致把本次的两个字节又复制进了Buffer,跟之前说的不读一样,会导致Buffer没有多余的空间放文件里的数据,导致一直读不完,发生死循环。
2.3.2:mark & reset
这两个方法放到一起说,因为mark跟reset不放在一起使用,没有任何意义。
- mark:用于标记当前
position
的位置 - reset:用于恢复被
mark
标记的position
的位置
例子:
1 | ByteBuffer buf = ByteBuffer.allocate(2); |
输出结果:
1 | 1133557799 |
会发现,上下两个打印都是是一样的数据。嘛,还是跟上面一样,再回顾一下,这里如果用compact
会怎么操作?如果使用compact
会打印如下语句:
1 | 1122334455667788 |
这个现在也很好理解了,因为重置了位置所以每个数据被打印了两次,由于mark
的原因,每次实际上相当于只读了一个数据,所以剩下的一个数据被顺延到下次循环里打印,以此类推。
2.3.3:equals & compareTo
- equals:用来比较两个Buffer是否相等,判等条件为
- 类型相同(byte、char等)
- 剩余元素个数相等
- 剩余元素相同
- compareTo:比较两个Buffer中剩余元素的大小,如果满足如下条件,则认为buffer1小于buffer2:
- buffer1中第一个与buffer2不相等的元素小于buffer2的那个元素
- 所有元素相同,但是buffer1先比buffer2耗尽
三、实例:边读边写
例子:将rua.txt里的内容在读的同时写入文件haha.txt里
1 | readFile = new RandomAccessFile("D:\\rua.txt", "r"); |
结果haha.txt里的内容为:123456789
四、Buffer的分类
类别 | 解释 |
---|---|
ByteBuffer | 支持存放字节类型数据,抽象类,有DirectByteBuffer 、HeapByteBuffer 、MappedByteBuffer 两个子类,下面进行说明 |
CharBuffer | 支持存放char类型数据 |
DoubleBuffer | 支持存放double类型数据 |
FloatBuffer | 支持存放float类型数据 |
IntBuffer | 支持存放int类型数据 |
LongBuffer | 支持存放long类型数据 |
ShortBuffer | 支持存放short类型数据 |
通过上表可以看到,Buffer有很多实现,其中大部分都对应一种基本类型,ByteBuffer
比较特殊,下面介绍下它的两个子类。
ByteBuffer —-> HeapByteBuffer
直接通过byte数组实现的在java堆上的缓冲区。
ByteBuffer —-> DirectByteBuffer
直接在java堆外申请的一块内存,将文件映射到该内存空间,在大文件读写方面的效率非常高。
ByteBuffer —–> MappedByteBuffer
同样写效率非常高。
关于这几个Buffer后续会专门整理一篇文章来写。