【特色探究】非易失性映射字节缓冲区

看了十多篇文章才写出来,列原作者肯定是是列不完了。这是: 提供复制操作的文章教我用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 targetFileChannelWritableByteChannel的间接子类,它们中间还有一个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连这些我都想了好久,不敢写出来误人子弟)

翻看源码,我们发现MappedByteBufferByteBuffer的直接子类。同样不能直接创建,但是这个类甚至不能依靠自己创建对象。我们可以通过FileChannel中的map()方法来创建

创建时,需要传入一个MapMode,一个position,一个size。MapMode是FileChannel的内部类,我们无需自己创建,里面有3个可用常量:READ_ONLYREAD_WRITEPRIVATE,分别表示只读可读写不更改文件

但是这里有一个很烦的地方:你用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——

哎这。。不小心写多了,读者朋友分两天看?

java
102 views
Comments
登录后评论
Sign In

倒数第一个代码段的第一条注释,原本最后几个字是“缓冲区chang‘du”,说这是政治敏感词,然后我改了缓冲区容量,没有敏感词了。这是为啥

·

零拷贝技术之一, transferTo()