1. 概要

このクイックチュートリアルでは、 JavaNIOライブラリで提供されるFileChannelクラスについて説明します。 FileChannelとByteBufferを使用してデータを読み書きする方法について説明します。

また、FileChannelおよびその他のファイル操作機能のいくつかを使用する利点についても説明します。

2. FileChannelの利点

FileChannelの利点は次のとおりです。

  • ファイル内の特定の位置での読み取りと書き込み
  • ファイルのセクションをメモリに直接ロードします。これにより、より効率的になります。
  • あるチャネルから別のチャネルにファイルデータをより高速に転送できます
  • ファイルのセクションをロックして、他のスレッドによるアクセスを制限できます
  • データの損失を回避するために、ファイルへの更新をすぐにストレージに強制的に書き込むことができます

3. FileChannelで読み取る

FileChannel は、大きなファイルを読み取るときに、標準のI/Oよりも高速に実行されます。

Java NIO の一部ですが、 FileChannel操作はをブロックしており、非ブロックモードがないことに注意してください。

3.1. FileChannelを使用してファイルを読み取る

以下を含むファイルでFileChannelを使用してファイルを読み取る方法を理解しましょう。

Hello world

このテストはファイルを読み取り、正しく読み取られたことを確認します。

@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);
    }
}

ここでは、を使用してファイルからバイトを読み取ります FileChannel RandomAccessFile 、 と ByteBuffer。

また、注意する必要があります複数の同時スレッドが使用できます FileChannelsを安全に 。 ただし、チャネルの位置の更新やファイルサイズの変更を伴う操作は、一度に1つのスレッドのみが許可されます。 これにより、前の操作が完了するまで、同様の操作を試みる他のスレッドがブロックされます。

ただし、明示的なチャネル位置を提供する操作は、ブロックされることなく同時に実行できます。

3.2. FileChannelを開く

FileChannel を使用してファイルを読み取るには、ファイルを開く必要があります。

RandomAccessFileを使用してFileChannelを開く方法を見てみましょう。

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

モード「r」は、チャネルが「読み取り専用に開いている」ことを示します。 を閉じることに注意する必要があります RandomAccessFile 関連するチャネルも閉じます。

次に、 FileChannel を開いて、FileInputStreamを使用してファイルを読み取ります。

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

同様に、 FileInputStream を閉じると、それに関連付けられているチャネルも閉じます。

3.3. FileChannelからのデータの読み取り

データを読み取るには、読み取りメソッドの1つを使用できます。

バイトのシーケンスを読み取る方法を見てみましょう。 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);

バイト配列をStringにデコードするためのCharsetの必要性に注意する必要があります。

バイトが最初にエンコードされた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 を使用してデータを書き込むには、writeメソッドのいずれかを使用できます。

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 を使用すると、読み取りまたは書き込みを行っている位置を取得および変更できます。

現在の位置を取得する方法を見てみましょう:

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 メソッドは、ファイルセクションのロックを取得しようとします。 要求されたファイルセクションがすでに別のスレッドによってブロックされている場合、OverlappingFileLockException例外がスローされます。 このメソッドは、 boolean パラメーターを取り、共有ロックまたは排他ロックのいずれかを要求します。

一部のオペレーティングシステムでは共有ロックが許可されておらず、デフォルトで排他ロックになっている場合があることに注意してください。

11. FileChannelを閉じる

最後に、 FileChannel の使用が終了したら、それを閉じる必要があります。 この例では、try-with-resources。を使用しました。

必要に応じて、 close メソッドを使用して、FileChannelを直接閉じることができます。

channel.close();

12. 結論

このチュートリアルでは、FileChannelを使用してファイルを読み書きする方法を見てきました。 さらに、ファイルサイズとその現在の読み取り/書き込み位置を読み取って変更する方法を検討し、並行アプリケーションまたはデータクリティカルなアプリケーションでFileChannelsを使用する方法を確認しました。

いつものように、例のソースコードはGitHubから入手できます。