看了十多篇文章才写出来,列原作者肯定是是列不完了。这是:
提供复制操作的文章,
教我用FileChannel
的文章,
教我用MappedByteBuffer
的文章
这个模块越往后越超纲。。。学不下去了就放弃吧,没关系的
看到的第一反应:这是啥?
这是一种特定的JDK映射模式,在JDK14后可以使用。老杜的介绍是:可使用FileChannel
创建出引用非易失性存储器的MappedByteBuffer
看不懂?看不懂没关系,能看懂才有鬼嘞。除此之外,老杜没有说一句关于这个特性的话,那让我们试图用常规学习思路来探究这个特性
FileChannel类初步了解:复制文件功能
在老杜的介绍中,我们不难推断,FileChannel用来创建东西,那么它大概率是特性的主体
通过面向CSDN编程的技巧【doge】,我们发现这个类其实可以用来复制文件。我们利用fis和fos来复杂的时候,需要连两条通道:源文件到内存,以及内存到目标文件。而使用FileChannel复制文件时,直接联通了源文件与目标文件,很适合复制那种几MB的文件,也就是在文件复制方面,FileChannel更有优势
复制时,我们还是需要fis和fos,然后直接获取两个文件的通道。验证了源码和注释后,我们发现:transferTo()
方法可以将源文件通道的内容移到目标文件通道,还有一个用法相似的方法:transferFrom()
至于transferTo方法,需要传入3个参数:数据开始传输的位置long position
(也就类似于起始下标,解释为起始字节),转移的长度long count
(复制时,这个数值等于源文件的长度,用size()
方法获取),目标通道WritableByteChannel target
(FileChannel
是WritableByteChannel
的间接子类,它们中间还有一个GatheringByteChannel
)
综上,这个复制算法应该这样写:
try (FileInputStream fis = new FileInputStream(pathIn);
FileOutputStream fos = new FileOutputStream(pathOut);
FileChannel input = fis.getChannel();
FileChannel output = fos.getChannel()) {
input.transferTo(0, input.size(), output);
} catch (IOException e) {
e.printStackTrace();
}
用transferFrom方法同理,自行尝试(believe you~)
FileChannel的读写功能
在复制操作中,我们已经了解了如何创建FileChannel对象,以及如何转移内容。作为文件通道类,它还能对文件进行更多较复杂的操作
我们通过源码,能发现:这个FileChannel同时拥有read和Write方法。这些方法不需要传入byte数组,char数组,或String,而需要传入一个ByteBuffer
对象。
ByteBuffer是一个抽象类,创建对象时,需要调用本类中的allocate()
方法。allocate意为“分配”,这个方法实际上是指定ByteBuffer的容量,并返回ByteBuffer对象
ByteBuffer buffer = ByteBuffer.allocate(64);
buffer
是一个容量64个字节的ByteBuffer对象
那么,我们可以从源文件中读取数据到buffer中。read方法会返回一个int表示读到的数据数量,-1表示没有读到
这是一步读取操作
try (var input = new FileInputStream(pathIn).getChannel()) {
int count = input.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
然后我们得到了一个存了数据的buffer,那有人说了:那就可以直接写进去了呗,然后哐哐哐一顿操作,定义了一个用来输出的output
对象,写了一句output.write(buffer)
。然后他说,写完了
不好意思,这个不是byte数组。ByteBuffer有一个特点:程序员不知道它一次能写出来多少字节
那咋办呢!!!我们发现,ByteBuffer类提供了一个方法:hasRemaining()
。这个方法是判断容量是否大于当前下标(也就是还有没有剩余空间)
又发现,提供了一个flip()
方法,能够将这个ByteBuffer的容量设置为当前的指针位置,然后把当前的指针位置设置为0。
这样操作后,我们可以放心地用hasRemaining()
方法检验剩余。所以我们只需要一直判断有没有剩余,一直有就一直写
buffer.flip()
while (buffer.hasRemaining()) {
output.write(buffer);
}
最后,旧版本的同学,也就是不能用try-with-resources格式的同学,记得关流,把那两个FileChannel的close方法调用一下
文件通道运行时的动作
按照常识,文件通道会记录些什么东西?
第一个,就是position,经测试,可以表述为:指针,也就是你这个文件读或者写到哪里了。当然从上面的用法中,可以发现ByteBuffer也有用途相似的position属性。position与我们常说的下标相似,未进行任何操作时,值为0
以input对象为例,我们可以获取position:
input.position();
也可以指定下一次它应该从第32个字节开始读:
input.position(32);
另外,文件的大小也是可以被获取的,单位为字节
long size = input.size();
FileChannel还提供了一个方法truncate()
,用于截取文件。使用时传入一个long数据表示截取的长度,运行后,文件中只会剩下指定长度的字节数,后面的字节会被全部舍弃。如果position指向了被舍弃的字节,那么它将会被移动到截断后文件的最后一个字节上
注意:如果你的FileChannel是通过InputSteam
创建的,这个方法不可用。因为方法的本质是更改文件,也就是“写入”(“输出”)
比如这段代码运行后,output通道指向的文件里只会剩下最前面的32个字节
output.truncate(32);
最后还有一个问题:我们使用输出流的时候,会有一个flush操作,目的是将内存中的数据全都赶进目标文件。那如果FileChannel输出时有字节赖在内存中,又没有flush方法来轰走它们,咋办?
有个类似的方法,叫做force()
,意为“强迫”内存中的字节写到硬盘上。但是这个force方法需要传入一个布尔参数,用于指定是否需要将文件的元数据写入硬盘(如一些权限信息,存储位置等信息,详细知识请咨询百度百科)
一般我们在写入完成后,会加上这行代码
output.force(true);
MappedByteBuffer的认识
(只有基操,更深的知识我看不懂qwq连这些我都想了好久,不敢写出来误人子弟)
翻看源码,我们发现MappedByteBuffer
是ByteBuffer
的直接子类。同样不能直接创建,但是这个类甚至不能依靠自己创建对象。我们可以通过FileChannel中的map()
方法来创建
创建时,需要传入一个MapMode,一个position,一个size。MapMode是FileChannel的内部类,我们无需自己创建,里面有3个可用常量:READ_ONLY
,READ_WRITE
,PRIVATE
,分别表示只读,可读写,不更改文件
但是这里有一个很烦的地方:你用FileInputStream
创建通道,给你报NonWritableChannelException
;你用FileOutputStream
创建通道,给你报NonReadableChannelException
,也就是你怎样都缺一个。其实ByteBuffer还有一种依赖形式:RandomAccessFile
,是它们的结合,也就没有这种问题(之前没讲因为感觉超纲了)
如我们可以这样创建一个指定模式为可读可写,从文件最开头开始映射(下标0),大小为64的MappedByteBuffer
对象:
try (var model = new RandomAccessFile(pathIn, "rw").getChannel()) {
MappedByteBuffer buffer = model.map(FileChannel.MapMode.READ_WRITE, 0, 64);
} catch (IOException e) {
e.printStackTrace();
}
rw表示可读可写,这保证了缓冲区也是可读可写的;这里第二个参数实际上是问你要将这个缓冲区的数据插在哪里,比如你写了一个2,那么当你改完缓冲区之后,里面的数据全都会在下标2的位置贴上;第三个参数表示缓冲区的大小,如果不进行任何操作,并且写入的数据小于这个缓冲区的大小,那么贴数据的时候还是会把整个缓冲区贴上去,于是你会看到很多长得像这样的字符:[NUL]
如果仅限于老杜的定义:可使用FileChannel
创建出引用非易失性存储器的MappedByteBuffer
,那其实已经讲完了。大家再回去看一眼那个名字巨长的特性:非易失性映射字节缓冲区,指的就是MappedByteBuffer
,它能够更持久地,更牢固地保存数据
而在行为上,映射字节缓冲区与普通的字节缓冲区没有区别。这一点在源码注释中的最后一句有提到:
Mapped byte buffers otherwise behave no differently than ordinary direct byte buffers.
这时,文件同时支持读写,而MappedByteBuffer又依附于创建者,所以我们可以对创建者读写。这里涉及到两类方法:put()
和get()
及其带后缀的衍生物(其实这两个在ByteBuffer里面也有,这里需要用到了就拿出来讲讲)。这两种方法功能对应,我列举一下put方法,至于get方法可以自行对照使用
与DataOutputStream相似,MappedByteBuffer提供了7个put方法,分别用来输出除byte以外的7种基础数据类型(没有字符串了!)。不同的是,这些方法各有一个重载方法,重载方法的第一个参数为下标,也就是比DataOutputStream多了一个指定位置的功能。(这部分方法并不建议用,因为基础数据类型中,占248字节的有很多,特别是char,打中文有乱码的风险,打英文会给你输出英文,但是后面会带一个[NUL]
,而且计算时认为这个英文字符它占两个字节。。。)
除此之外,还有无后缀的put方法。可以向里面传入一个byte,也有两个要传入ByteBuffer的,分别是只传ByteBuffer和位置,ByteBuffer,起始下标,长度,而对于需传入byte数组的方法,分为只传数组和数组和区间,这两个方法分别有可以指定位置的重载方法(这部分方法是建议使用的,不会把握不住乱输出)
嗯。。。学过流的同学们应该都会使用的吧,也蛮好记的。就是这个数量。。。有22个之多。。。
使用时完全可以按直觉使用,就是注意一下需要ByteBuffer为参数的方法是特殊的:不给位置就别想指定区间
put方法过后,记得调用一下MappedByteBuffer
独有的无参force()方法将数据刷新进去(如你所见,对缓冲区操作的时候,硬盘也会被改写)另外,为了防止字符[NUL]
的出现,用这个方法写入的时候please提前算出需要插入数据的准确数量,不要妄想用flip方法,我试过好几次,不顶用的qwq
示例部分:我们要一个读写模式的缓冲区,在pathIn文件末尾(加上的第一个字母的下标等于原文件的长度,所以指针设为原文件的长度值)加上2个byte:cd
try (var model = new RandomAccessFile(pathIn, "rw").getChannel()) {
// 三个参数分别表示:设置为可读写模式,添加位置(末尾),缓冲区容量
MappedByteBuffer buffer = model.map(FileChannel.MapMode.READ_WRITE, model.size(), 2);
// 放入字符c和字符d
buffer.put((byte) 'c');
buffer.put((byte) 'd');
// 刷新一下
buffer.force();
} catch (IOException e) {
e.printStackTrace();
}
finally——
哎这。。不小心写多了,读者朋友分两天看?