1. 概要

この記事では、Java7の非同期ファイルチャネルAPIの新しいI/ O(NIO2)の主要な追加APIの1つについて説明します。

一般に非同期チャネルAPIを初めて使用する場合は、このサイトに紹介記事があり、先に進む前にこのリンクをたどることで読むことができます。

NIO.2 ファイル操作およびパス操作についても詳しく読むことができます。これらを理解すると、この記事をより簡単に理解できるようになります。

プロジェクトでNIO2非同期ファイルチャネルを使用するには、必要なすべてのクラスがバンドルされているため、java.nio.channelsパッケージをインポートする必要があります。

import java.nio.channels.*;

2. 非同期ファイルチャネル

このセクションでは、ファイルに対して非同期操作を実行できるようにするメインクラスであるAsynchronousFileChannelクラスの使用方法について説明します。 そのインスタンスを作成するには、静的openメソッドを呼び出します。

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  filePath, READ, WRITE, CREATE, DELETE_ON_CLOSE);

すべての列挙値は、Sta ndardOpenOptionから取得されます。

オープンAPIの最初のパラメーターは、ファイルの場所を表すPathオブジェクトです。 NIO2でのパス操作の詳細については、このリンクを参照してください。 他のパラメータは、返されたファイルチャネルで使用できるオプションを指定するセットを構成します。

作成した非同期ファイルチャネルを使用して、ファイルに対して既知のすべての操作を実行できます。 操作のサブセットのみを実行するには、それらのみのオプションを指定します。 たとえば、次のように読みます。

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  filePath, StandardOpenOption.READ);

3. ファイルからの読み取り

NIO2のすべての非同期操作と同様に、ファイルの内容の読み取りは2つの方法で実行できます。 FutureCompletionHandlerを使用します。 いずれの場合も、返されたチャネルの readAPIを使用します。

mavenのテストリソースフォルダー内、またはmavenを使用していない場合はソースディレクトリ内に、baeldung.comというテキストのみを先頭にしたfile.txtというファイルを作成しましょう。 次に、このコンテンツの読み方を示します。

3.1. 将来のアプローチ

まず、Futureクラスを使用してファイルを非同期で読み取る方法を説明します。

@Test
public void givenFilePath_whenReadsContentWithFuture_thenCorrect() {
    Path path = Paths.get(
      URI.create(
        this.getClass().getResource("/file.txt").toString()));
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    Future<Integer> operation = fileChannel.read(buffer, 0);

    // run other code as operation continues in background
    operation.get();
      
    String fileContent = new String(buffer.array()).trim();
    buffer.clear();

    assertEquals(fileContent, "baeldung.com");
}

上記のコードでは、ファイルチャネルを作成した後、 read APIを使用します。このAPIは、 ByteBuffer を使用して、チャネルから読み取ったコンテンツを最初のパラメーターとして保存します。

2番目のパラメーターは、読み取りを開始するファイル内の位置を示すlongです。

このメソッドは、ファイルが読み取られたかどうかに関係なく、すぐに戻ります。

次に、操作がバックグラウンドで続行されるため、他のコードを実行できます。 他のコードの実行が終了したら、 get() APIを呼び出すことができます。このAPIは、他のコードを実行していたときに操作がすでに完了している場合はすぐに返されます。そうでない場合は、操作が完了するまでブロックされます。

私たちの主張は確かに、ファイルの内容が読み取られたことを証明しています。

read API呼び出しの位置パラメーターをゼロから別の値に変更した場合も、その効果がわかります。 たとえば、文字列baeldung.comの7番目の文字はgです。 したがって、位置パラメータを7に変更すると、バッファに文字列g.comが含まれるようになります。

3.2. CompleteHandlerアプローチ

次に、CompletionHandlerインスタンスを使用してファイルの内容を読み取る方法を確認します。

@Test
public void 
  givenPath_whenReadsContentWithCompletionHandler_thenCorrect() {
    
    Path path = Paths.get(
      URI.create( this.getClass().getResource("/file.txt").toString()));
    AsynchronousFileChannel fileChannel 
      = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    fileChannel.read(
      buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // result is number of bytes read
            // attachment is the buffer containing content
        }
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {

        }
    });
}

上記のコードでは、 readAPIの2番目のバリアントを使用しています。 それでも、ByteBuffer読み取り操作の開始位置がそれぞれ最初と2番目のパラメーターとして使用されます。 3番目のパラメーターはCompletionHandlerインスタンスです。

完了ハンドラーの最初の汎用タイプは、操作の戻りタイプです。この場合、読み取られたバイト数を表す整数です。

2つ目は、アタッチメントのタイプです。 read が完了したときに、completeコールバックAPI内のファイルのコンテンツを使用できるようにバッファーをアタッチすることを選択しました。

意味的に言えば、 complete コールバックメソッド内でアサーションを実行できないため、これは実際には有効な単体テストではありません。 ただし、これは一貫性を保つためであり、コードを可能な限りcopy-paste-run-可能にするためです。

4. ファイルへの書き込み

Java NIO2では、ファイルに対して書き込み操作を実行することもできます。 他の操作で行ったように、2つの方法でファイルに書き込むことができます。 FutureCompletionHandlerを使用します。 いずれの場合も、返されたチャネルの writeAPIを使用します。

ファイルに書き込むためのAsynchronousFileChannelの作成は、次のように実行できます。

AsynchronousFileChannel fileChannel
  = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

4.1. 特別な考慮事項

openAPIに渡されたオプションに注意してください。 path で表されるファイルがまだ存在しない場合に作成する場合は、別のオプションStandardOpenOption.CREATEを追加することもできます。 もう1つの一般的なオプションは、 StandardOpenOption.APPEND で、ファイル内の既存のコンテンツを上書きしません。

テスト目的でファイルチャネルを作成するために、次の行を使用します。

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  path, WRITE, CREATE, DELETE_ON_CLOSE);

このようにして、任意のパスを提供し、ファイルが確実に作成されるようにします。 テストが終了すると、作成されたファイルは削除されます。 テストの終了後に作成されたファイルが削除されないようにするには、最後のオプションを削除します。

アサーションを実行するには、アサーションに書き込んだ後、可能な場合はファイルの内容を読み取る必要があります。 冗長性を回避するために、別のメソッドで読み取るためのロジックを非表示にしましょう。

public static String readContent(Path file) {
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      file, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    Future<Integer> operation = fileChannel.read(buffer, 0);

    // run other code as operation continues in background
    operation.get();     

    String fileContent = new String(buffer.array()).trim();
    buffer.clear();
    return fileContent;
}

4.2. Futureアプローチ

Future クラスを使用して非同期でファイルに書き込むには:

@Test
public void 
  givenPathAndContent_whenWritesToFileWithFuture_thenCorrect() {
    
    String fileName = UUID.randomUUID().toString();
    Path path = Paths.get(fileName);
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, WRITE, CREATE, DELETE_ON_CLOSE);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    buffer.put("hello world".getBytes());
    buffer.flip();

    Future<Integer> operation = fileChannel.write(buffer, 0);
    buffer.clear();

    //run other code as operation continues in background
    operation.get();

    String content = readContent(path);
    assertEquals("hello world", content);
}

上記のコードで何が起こっているかを調べてみましょう。 ランダムなファイル名を作成し、それを使用してPathオブジェクトを取得します。 このパスを使用して、前述のオプションで非同期ファイルチャネルを開きます。

次に、ファイルに書き込みたいコンテンツをバッファに入れ、writeを実行します。 ヘルパーメソッドを使用してファイルの内容を読み取り、実際にそれが期待どおりであることを確認します。

4.3. CompleteHandlerアプローチ

完了ハンドラーを使用して、whileループで操作が完了するのを待つ必要がないようにすることもできます。

@Test
public void 
  givenPathAndContent_whenWritesToFileWithHandler_thenCorrect() {
    
    String fileName = UUID.randomUUID().toString();
    Path path = Paths.get(fileName);
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, WRITE, CREATE, DELETE_ON_CLOSE);

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put("hello world".getBytes());
    buffer.flip();

    fileChannel.write(
      buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // result is number of bytes written
            // attachment is the buffer
        }
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {

        }
    });
}

今回書き込みAPIを呼び出すとき、唯一の新しいことは、タイプCompletionHandlerの匿名内部クラスを渡す3番目のパラメーターです。

操作が完了すると、クラスはそのcompletedメソッドを呼び出し、その中で何が起こるかを定義できます。

5. 結論

この記事では、JavaNIO2の非同期ファイルチャネルAPIの最も重要な機能のいくつかについて説明しました。

この記事のすべてのコードスニペットと完全なソースコードを入手するには、Githubプロジェクトにアクセスしてください。