FileChannel指南

鍋外的大佬發表於2019-05-25

推薦關注公眾號:鍋外的大佬

每日推送國外技術好文,幫助每位開發者更優秀地成長

原文連結: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上找到。