• FileChannel指南


    推荐关注公众号:锅外的大佬

    每日推送国外技术好文,帮助每位开发者更优秀地成长

    原文链接:https://www.baeldung.com/java-filechannel

    作者:baeldung

    译者:Leesen

    1.概述

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

    2.FileChannel的优点

    FileChannel的优点包括:

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

    3.FileChannel读操作

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

    3.1.使用FileChannel读取文件

    先了解如何使用FileChannel读取一个文件,该文件包含:

     Hello world
    

    下面测试读取文件,并检查是否ok:

    @Test
    public void givenFile_whenReadWithFileChannelUsingRandomAccessFile_thenCorrect() 
      throws IOException {
        try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
            FileChannel channel = reader.getChannel();
            ByteArrayOutputStream out = new ByteArrayOutputStream()) {
    
            int bufferSize = 1024;
            if (bufferSize > channel.size()) {
               bufferSize = (int) channel.size();
            }
            ByteBuffer buff = ByteBuffer.allocate(bufferSize);
    
            while (channel.read(buff) > 0) {
                out.write(buff.array(), 0, buff.position());
                buff.clear();
            }
    
         String fileContent = new String(out.toByteArray(), StandardCharsets.UTF_8);
    
         assertEquals("Hello world", fileContent);
        }
    }
    

    这里使用FileChannelRandomAccessFileByteBuffer从文件中读取字节。还应该注意,多个并发线程可以安全地使用FileChannel。但是,每次只允许一个线程执行涉及更新通道位置(channel position)或更改其文件大小的操作。这会阻止其他试图执行类似操作的线程,直到前一个操作完成。
    但是,显式提供通道位置的操作可以并发运行且不会被阻塞。

    3.2.打开FileChannel

    为了使用FileChannel读取文件,我们必须打开它(Open FileChannel)。看看如何使用RandomAccessFile打开FileChannel:

    RandomAccessFile reader = new RandomAccessFile(file, "r");
    FileChannel channel = reader.getChannel();
    

    模式“r”表示通道仅为“只读“,注意,关闭RandomAccessFile也将关闭与之关联的通道。
    接下来,使用FileInputStream打开一个FileChannel来读取文件:

    FileInputStream fin= new FileInputStream(file);
    FileChannel channel = fin.getChannel();
    

    同样的,关闭FileInputStream也会关闭与之相关的通道。

    3.3.从FileChannel中读取数据

    为了读取数据,我们可以使用只读模式。接下来看看如何读取字节序列,我们将使用ByteBuffer来保存数据:

    ByteBuffer buff = ByteBuffer.allocate(1024);
    int noOfBytesRead = channel.read(buff);
    String fileContent = new String(buff.array(), StandardCharsets.UTF_8);
    
    assertEquals("Hello world", fileContent);
    

    然后,我们将看到如何从文件某个位置开始读取一个字节序列:

    ByteBuffer buff = ByteBuffer.allocate(1024);
    int noOfBytesRead = channel.read(buff, 5);
    String fileContent = new String(buff.array(), StandardCharsets.UTF_8);
    assertEquals("world", fileContent);
    

    我们应该注意:需要使用字符集(Charset)将字节数组解码为字符串
    我们指定原始编码字节的字符集。没有它,我们可能会以断章取义的文字结束。特别是像UTF-8UTF-16这样的多字节编码可能无法解码文件的任意部分,因为一些多字节字符可能是不完整的。

    4.FileChannel写操作

    4.1.使用FileChannel写入文件

    我们来探究下如何使用FileChannel写:

    @Test
    public void whenWriteWithFileChannelUsingRandomAccessFile_thenCorrect()   
      throws IOException {
        String file = "src/test/resources/test_write_using_filechannel.txt";
        try (RandomAccessFile writer = new RandomAccessFile(file, "rw");
            FileChannel channel = writer.getChannel()){
            ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
    
            channel.write(buff);
    
         // verify
         RandomAccessFile reader = new RandomAccessFile(file, "r");
         assertEquals("Hello world", reader.readLine());
         reader.close();
        }
    }
    

    4.2.打开FileChannel

    要使用FileChannel写入文件,必须先打开它。使用RandomAccessFile打开一个FileChannel:

    RandomAccessFile writer = new RandomAccessFile(file, "rw");
    FileChannel channel = writer.getChannel();
    

    模式“rw”表示通道为“读写”。
    使用FileOutputStream打开FileChannel:

    FileOutputStream fout = new FileOutputStream(file);
    FileChannel channel = fout.getChannel();
    

    4.3.FileChannel写入数据

    使用FileChannel写数据,可以使用其中的某个写方法。
    我们来看下如何写一个字节序列,使用ByteBuffer来存储数据:

    ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
    channel.write(buff);
    

    接下来,我们将看到如何从文件某个位置开始写一个字节序列:

    ByteBuffer buff = ByteBuffer.wrap("Hello world".getBytes(StandardCharsets.UTF_8));
    channel.write(buff, 5);
    

    5.当前位置

    FileChannel允许我们获得和改变读或写的位置(position)。获得当前的位置:

    long originalPosition = channel.position();
    

    设置位置:

    channel.position(5);
    assertEquals(originalPosition + 5, channel.position());
    

    6.获取文件大小

    使用FileChannel.size方法获取文件大小(以字节为单位):

    @Test
    public void whenGetFileSize_thenCorrect() 
      throws IOException {
        RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
        FileChannel channel = reader.getChannel();
    
        // the original file size is 11 bytes.
        assertEquals(11, channel.size());
    
        channel.close();
        reader.close();
    }
    

    7.截断文件

    使用FileChannel.truncate方法将文件截断为给定的大小(以字节为单位):

    @Test
    public void whenTruncateFile_thenCorrect() throws IOException {
        String input = "this is a test input";
    
        FileOutputStream fout = new FileOutputStream("src/test/resources/test_truncate.txt");
        FileChannel channel = fout.getChannel();
    
        ByteBuffer buff = ByteBuffer.wrap(input.getBytes());
        channel.write(buff);
        buff.flip();
    
        channel = channel.truncate(5);
        assertEquals(5, channel.size());
    
        fout.close();
        channel.close();
    }
    

    8.强制更新

    由于性能原因,操作系统可能缓存文件更改,如果系统崩溃,数据可能会丢失。要强制文件内容和元数据不断写入磁盘,我们可以使用force方法:

    channel.force(true);
    

    仅当文件存储在本地设备上时,才能保证该方法有效。

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

    使用FileChannel.map方法将文件的部分加载到内存中。使用FileChannel.MapMode.READ_ONLY以只读模式打开文件:

    @Test
    public void givenFile_whenReadAFileSectionIntoMemoryWithFileChannel_thenCorrect() throws IOException { 
        try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "r");
            FileChannel channel = reader.getChannel();
            ByteArrayOutputStream out = new ByteArrayOutputStream()) {
    
            MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 6, 5);
    
            if(buff.hasRemaining()) {
              byte[] data = new byte[buff.remaining()];
              buff.get(data);
              assertEquals("world", new String(data, StandardCharsets.UTF_8));  
            }
        }
    }
    

    类似地,可以使用FileChannel.MapMode.READ_WRITE以读写模式打开文件。还可以使用FileChannel.MapMode.PRIVATE模式,该模式下,更改不应用于原始文件。

    10.锁定文件部分

    来看下如何锁定文件某一部分,使用FileChannel.tryLock方法阻止对文件某一部分进行高并发访问。

    @Test
    public void givenFile_whenWriteAFileUsingLockAFileSectionWithFileChannel_thenCorrect() throws IOException { 
        try (RandomAccessFile reader = new RandomAccessFile("src/test/resources/test_read.in", "rw");
            FileChannel channel = reader.getChannel();
            FileLock fileLock = channel.tryLock(6, 5, Boolean.FALSE )){
    
            //do other operations...
    
            assertNotNull(fileLock);
        }
    }
    

    tryLock方法尝试获取文件部分(file section)上的锁。如果请求的文件部分已被另一个线程阻塞,它将抛出一个OverlappingFileLockException异常。此方法还接受Boolean参数来请求共享锁或独占锁。
    我们应该注意到,有些操作系统可能不允许共享锁,默认情况下是独占锁。

    11.FileChannel关闭

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

    channel.close();
    

    12.总结

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

  • 相关阅读:
    遇到的开发错误
    我的麦本本配置
    C#:100以内能被7整除的最大自然数
    C#:静态字段和静态方法的学习
    Oracle 备份、恢复单表或多表数据步骤 (转)
    有关关键路径的概念和算法 (转)
    Delphi中StringReplace函数的使用
    Delphi 里 FillChar的用法
    Delphi中destroy, free, freeAndNil, release用法和区别
    项目经理、系统架构师或技术骨干应该具备的水平
  • 原文地址:https://www.cnblogs.com/liululee/p/10921299.html
Copyright © 2020-2023  润新知