点击左上角,关注:“锅外的大佬”
专注分享国外最新技术内容
帮助每位开发者更优秀地成长

1.概述

在这篇速学教程中,我们将研究 JavaNIO库中提供的 FileChannel类,讨论如何使用 FileChannel和 ByteBuffer读写数据,探讨使用 FileChannel以及其他文件操作特性的优点。

2.FileChannel的优点

FileChannel的优点包括:
  • 在文件特定位置进行读写操作
  • 将文件一部分直接加载到内存,这样效率更高
  • 以更快的速度将文件数据从一个通道传输到另一个通道
  • 锁定文件的某一部分来限制其他线程访问
  • 为了避免数据丢失,强制立即将更新写入文件并存储

3.FileChannel读操作

当我们读取一个大文件时, FileChannel比 标准I/O执行得更快。需要注意,虽然 FileChannel是 JavaNIO的一部分,但是 FileChannel操作是阻塞的,并且没有非阻塞模式。

3.1.使用FileChannel读取文件

先了解如何使用 FileChannel读取一个文件,该文件包含:
  1. Hello world
下面测试读取文件,并检查是否ok:
  1. @Test
  2. publicvoid givenFile_whenReadWithFileChannelUsingRandomAccessFile_thenCorrect()
  3. throwsIOException{
  4. try(RandomAccessFile reader =newRandomAccessFile("src/test/resources/test_read.in","r");
  5. FileChannel channel = reader.getChannel();
  6. ByteArrayOutputStream out =newByteArrayOutputStream()){
  7. int bufferSize =1024;
  8. if(bufferSize > channel.size()){
  9. bufferSize =(int) channel.size();
  10. }
  11. ByteBuffer buff =ByteBuffer.allocate(bufferSize);
  12. while(channel.read(buff)>0){
  13. out.write(buff.array(),0, buff.position());
  14. buff.clear();
  15. }
  16. String fileContent =newString(out.toByteArray(),StandardCharsets.UTF_8);
  17. assertEquals("Hello world", fileContent);
  18. }
  19. }
这里使用 FileChannel、 RandomAccessFile和 ByteBuffer从文件中读取字节。还应该注意,多个并发线程可以安全地使用 FileChannel。但是,每次只允许一个线程执行涉及更新通道位置( channel position)或更改其文件大小的操作。这会阻止其他试图执行类似操作的线程,直到前一个操作完成。 但是,显式提供通道位置的操作可以并发运行且不会被阻塞。

3.2.打开FileChannel

为了使用 FileChannel读取文件,我们必须打开它( OpenFileChannel)。看看如何使用 RandomAccessFile打开 FileChannel:
  1. RandomAccessFile reader =newRandomAccessFile(file,"r");
  2. FileChannel channel = reader.getChannel();
模式“r”表示通道仅为“只读“,注意,关闭 RandomAccessFile也将关闭与之关联的通道。 接下来,使用 FileInputStream打开一个 FileChannel来读取文件:
  1. FileInputStream fin=newFileInputStream(file);
  2. FileChannel channel = fin.getChannel();
同样的,关闭 FileInputStream也会关闭与之相关的通道。

3.3.从FileChannel中读取数据

为了读取数据,我们可以使用只读模式。接下来看看如何读取字节序列,我们将使用 ByteBuffer来保存数据:
  1. ByteBuffer buff =ByteBuffer.allocate(1024);
  2. int noOfBytesRead = channel.read(buff);
  3. String fileContent =newString(buff.array(),StandardCharsets.UTF_8);
  4. assertEquals("Hello world", fileContent);
然后,我们将看到如何从文件某个位置开始读取一个字节序列:
  1. ByteBuffer buff =ByteBuffer.allocate(1024);
  2. int noOfBytesRead = channel.read(buff,5);
  3. String fileContent =newString(buff.array(),StandardCharsets.UTF_8);
  4. assertEquals("world", fileContent);
我们应该注意:需要使用字符集(Charset)将字节数组解码为字符串 。
指定原始编码字节的字符集,没有它,我们可能会以断章取义的文字结束。特别是像 UTF-8和 UTF-16这样的多字节编码可能无法解码文件的任意部分,因为一些多字节字符可能是不完整的。

4.FileChannel写操作

4.1.使用FileChannel写入文件

我们来探究下如何使用 FileChannel写:
  1. @Test
  2. publicvoid whenWriteWithFileChannelUsingRandomAccessFile_thenCorrect()
  3. throwsIOException{
  4. String file ="src/test/resources/test_write_using_filechannel.txt";
  5. try(RandomAccessFile writer =newRandomAccessFile(file,"rw");
  6. FileChannel channel = writer.getChannel()){
  7. ByteBuffer buff =ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
  8. channel.write(buff);
  9. // verify
  10. RandomAccessFile reader =newRandomAccessFile(file,"r");
  11. assertEquals("Hello world", reader.readLine());
  12. reader.close();
  13. }
  14. }

4.2.打开FileChannel

要使用 FileChannel写入文件,必须先打开它。使用 RandomAccessFile打开一个 FileChannel:
  1. RandomAccessFile writer =newRandomAccessFile(file,"rw");
  2. FileChannel channel = writer.getChannel();
模式“rw”表示通道为“读写”。 使用 FileOutputStream打开 FileChannel:
  1. FileOutputStream fout =newFileOutputStream(file);
  2. FileChannel channel = fout.getChannel();

4.3.FileChannel写入数据

使用 FileChannel写数据,可以使用其中的某个写方法。 我们来看下如何写一个字节序列,使用 ByteBuffer来存储数据:
  1. ByteBuffer buff =ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
  2. channel.write(buff);
接下来,我们将看到如何从文件某个位置开始写一个字节序列:
  1. ByteBuffer buff =ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
  2. channel.write(buff,5);

5.当前位置

FileChannel允许我们获得和改变读或写的位置( position)。获得当前的位置:
  1. long originalPosition = channel.position();
设置位置:
  1. channel.position(5);
  2. assertEquals(originalPosition +5, channel.position());

6.获取文件大小

使用 FileChannel.size方法获取文件大小(以字节为单位):
  1. @Test
  2. publicvoid whenGetFileSize_thenCorrect()
  3. throwsIOException{
  4. RandomAccessFile reader =newRandomAccessFile("src/test/resources/test_read.in","r");
  5. FileChannel channel = reader.getChannel();
  6. // the original file size is 11 bytes.
  7. assertEquals(11, channel.size());
  8. channel.close();
  9. reader.close();
  10. }

7.截断文件

使用 FileChannel.truncate方法将文件截断为给定的大小(以字节为单位):
  1. @Test
  2. publicvoid whenTruncateFile_thenCorrect()throwsIOException{
  3. String input ="this is a test input";
  4. FileOutputStream fout =newFileOutputStream("src/test/resources/test_truncate.txt");
  5. FileChannel channel = fout.getChannel();
  6. ByteBuffer buff =ByteBuffer.wrap(input.getBytes());
  7. channel.write(buff);
  8. buff.flip();
  9. channel = channel.truncate(5);
  10. assertEquals(5, channel.size());
  11. fout.close();
  12. channel.close();
  13. }

8.强制更新

由于性能原因,操作系统可能缓存文件更改,如果系统崩溃,数据可能会丢失。要强制文件内容和元数据不断写入磁盘,我们可以使用 force方法:
  1. channel.force(true);
仅当文件存储在本地设备上时,才能保证该方法有效。

9.将文件部分加载到内存

使用 FileChannel.map方法将文件的部分加载到内存中。使用 FileChannel.MapMode.READ_ONLY以只读模式打开文件:
  1. @Test
  2. publicvoid givenFile_whenReadAFileSectionIntoMemoryWithFileChannel_thenCorrect()throwsIOException{
  3. try(RandomAccessFile reader =newRandomAccessFile("src/test/resources/test_read.in","r");
  4. FileChannel channel = reader.getChannel();
  5. ByteArrayOutputStream out =newByteArrayOutputStream()){
  6. MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY,6,5);
  7. if(buff.hasRemaining()){
  8. byte[] data =newbyte[buff.remaining()];
  9. buff.get(data);
  10. assertEquals("world",newString(data,StandardCharsets.UTF_8));
  11. }
  12. }
  13. }
类似地,可以使用 FileChannel.MapMode.READ_WRITE以读写模式打开文件。还可以使用 FileChannel.MapMode.PRIVATE模式,该模式下,更改不应用于原始文件。

10.锁定文件部分

来看下如何锁定文件某一部分,使用 FileChannel.tryLock方法阻止对文件某一部分进行高并发访问。
  1. @Test
  2. publicvoid givenFile_whenWriteAFileUsingLockAFileSectionWithFileChannel_thenCorrect()throwsIOException{
  3. try(RandomAccessFile reader =newRandomAccessFile("src/test/resources/test_read.in","rw");
  4. FileChannel channel = reader.getChannel();
  5. FileLock fileLock = channel.tryLock(6,5,Boolean.FALSE )){
  6. //do other operations...
  7. assertNotNull(fileLock);
  8. }
  9. }
tryLock方法尝试获取文件部分( file section)上的锁。如果请求的文件部分已被另一个线程阻塞,它将抛出一个 OverlappingFileLockException异常。此方法还接受 Boolean参数来请求共享锁或独占锁。 我们应该注意到,有些操作系统可能不允许共享锁,默认情况下是独占锁。

11.FileChannel关闭

最后,当使用FileChannel时,必须关闭它。在示例中,我们使用了 try-with-resources。 如果有必要,我们可以直接使用 FileChannel.close方法:
  1. channel.close();

12.总结

在本教程中,我们了解了如何使用 FileChannel读取和写入文件。此外,我们还研究了如何读取和更改文件大小及其当前读/写位置,并研究了如何在并发应用程序或数据关键应用程序中使用 FileChannel。与往常一样,示例的源代码可以在GitHub上找到。
原文链接:https://www.baeldung.com/java-filechannel
作者:baeldung
译者:Leesen




点击在看,和我一起帮助更多开发者!
继续阅读
阅读原文