チャネルのクラス構成

先週はバッファだけでしたが,今週はチャネルも組みあわせて使っていきましょう。

チャネルはストリームの代わりになるクラス群です。大もとになるのはjava.nio.channels.Channelインタフェースです。とはいうものの,ChannelインタフェースにはcloseメソッドとisOpenメソッドしか定義されていません。実際の入出力は,Channelインタフェースから派生したインタフェースを使用します。

入力はReadableByteChannelインタフェース,出力はWritableByteChannelインタフェースで定義します。このほか,大量の入力を扱うScatteringByteChannelインタフェース,同様に大量の出力を扱うGatheringByteChannelインタフェースが提供されています。

これらのインタフェースはすべて名前にByteを含んでいるのに気付くと思います。そう,これらのインタフェースを使う場合にはすべて,ByteBufferクラスを使用するのです。

これらのインタフェースをインプリメントしているクラスにはFileChannelクラス,DatagramChannelクラス,SocketChannelクラスなどがあります。

これらをまとめたものを図1に示します。

チャネルの構成
図1 チャネルのクラス構成

ここではFileChannelクラスを使用して,入出力処理を行っていきます。ソケットに関しては来月,取り上げる予定です。

FileChannel クラスを使用したファイルの読み書き

FileChannelクラスを使うには,まずオブジェクトを生成しなくてはなりません。しかし,FileChannelオブジェクトはnewで生成することはできません。必ず基になるストリームからgetChannelメソッドを使用して,FileChannelオブジェクトを取得するようにします。

FileChannelオブジェクトを取得できるストリームのクラスはFileInputStreamクラス,FileOutputStreamクラス,RandomAccessFileクラスの三つです。

  // 読み込み専用のFileChannelオブジェクト
  FileInputStream inStream = new FileInputStream(...);
  FIleChannel inChannel = inStream.getChannel();
  
  // 書き込み専用のFileChannelオブジェクト
  FileOutputStream outStream = new FileOutputStream(...);
  FIleChannel outChannel = outStream.getChannel();
 
  // 読み書き可能なFileChannelオブジェクト
  RandomAccessFile rwFile = new RandomAccessFile(..., "rw");
  FIleChannel rwChannel = rwFile.getChannel();

FileChannelクラスはReadableByteChannelインタフェースとWritableByteChannelインタフェースの両方をインプリメントしているので,読み込み・書き込みともに可能です。ただし,FileInputStreamオブジェクトからFileChannelオブジェクトを取得した場合,読み込み専用になります。同様に,FileOutputStreamオブジェクトから取得した場合には,書き込み専用になります。

RandomAccessFileクラスにもgetChannelメソッドが定義されています。この場合は,RandomAccessFileオブジェクトを生成するときのモードによってFileChannelオブジェクトのモードも決まります。上記の例では"rw"でRandomAccessFileを生成しているので,FileChannelオブジェクトも読み書きの両方が可能です。

チャネルを使った読み込みはreadメソッド,書き込みはwriteメソッドで行います。両方とも引数はByteBufferオブジェクトです。

読み込んだデータはByteBufferオブジェクトのpositionからlimitの間に格納されます。positionは読みこんだバイト数分だけ移動します。

書き込みの場合も,ByteBufferオブジェクトのpositionからlimitの間の要素が書き込まれます。同様にpositionは書き込んだバイト数分だけ移動します。

といっても,実際の例を見ないとわかりにくいですね。そこで,先々週にとりあげたファイル・コピーを行うサンプルで具体的な使い方を見ていきましょう。サンプルは3種類ありましたが,ダイレクトバッファを使用したものから説明します。

ファイルのコピーを行っているのはcopyメソッドです。

まず,チャネル・オブジェクトの取得を行います。ここでは,コピー元をFileInputStreamオブジェクトから,コピー先をFileOutputStreamオブジェクトから取得しています。

  public void copy(String srcFile, String destFile)
                                     throws IOException {
      // コピー元ファイル
      FileChannel srcChannel 
          = new FileInputStream(srcFile).getChannel();
      // コピー先ファイル
      FileChannel destChannel
          = new FileOutputStream(destFile).getChannel();

チャネル・オブジェクトが用意できたので,入出力用のバッファを用意します。ここではパフォーマンスを考慮してダイレクトバッファを使用していますが,ByteBuffer#allocateメソッドを使用して取得するバッファでもかまいません。

        // バッファの生成
        ByteBuffer buffer
            = ByteBuffer.allocateDirect(BUF_SIZE);

BUF_SIZEを4096にしたので,バッファのcapacityは4096になります。ここで生成したバッファは,ファイルのコピーが終わるまで使い回します。ダイレクトバッファは特にアロケーションのコストが高いので,なるべく長期間にわたって使うようにします。

さて,ここからが入出力を行うループです。

        while (true) {
            // バッファのクリア
            buffer.clear();
                 
            // ファイルから読み込み
            // 読みこんだバイト数分だけpositionが移動
            // 最後まで読み込んだ場合,戻り値が-1になる
            if (srcChannel.read(buffer) < 0) {
                break;
            }
                 
            // バッファのposition,limitを入れかえる
            // limit = position
            // position = 0
            buffer.flip();

            // ファイルに書き込み
            // position から limit の間の要素を書きこむ
            destChannel.write(buffer);
        }

はじめにclearメソッドで,bufferのpositionを0に,limitをcapacityにします。ただし,すでにバッファに格納されているデータはクリアされないのでご注意ください。

そして,ファイルから読み込みを行います。前述したように,readメソッドの戻り値は読み込んだバイト数になります。ファイルの最後に到達したときには戻り値は-1になるので,ループを抜けます。

次のflipメソッドがキーポイントです。flipメソッドはlimitをpositionの位置に移動させ,positionを0に移動させるメソッドです。FileChannel#readメソッドで読み込みを行うと,読み込んだバイト数だけpositionが移動します。つまり,読み込んだデータは0からpositionの位置までに格納されています。

この書き込みのときには,positionの位置以上に書き込む必要はありません。そこで,読み込み後のpositionの位置をlimitにすることで,読みこんだバイト数分だけ書き込みを行えます。

書き込みを行うと,再びlimitの位置までpositionが移動します。ここで移動したpositionとlimitはループの先頭で再びクリアされます。

最後にチャネルのクローズを行います。

        srcChannel.close();
        destChannel.close();

コードだけだと,bufferのpositionとlimitがどのように推移するかわかりにくいので,図にしてみました(図2)。図2はISO-8859-1で"abcdefghij"と書かれている10バイトのファイルを読みこんだときの,bufferのpositionとlimitを表しています。

position,limitの推移
図2 position,limitの推移

これをまとめると,基本的なバッファとチャネルの使用法は

  1. バッファのクリア
  2. チャネルからの読み込み(バッファへの書き込み)
  3. バッファのフリップ
  4. バッファからの読み込み

になります。ここで使用したサンプルでは,2.がファイルからの読み込み,4.がファイルへの書き込み処理になっています。

FileChannelクラスの特殊な使い方

チャネルとバッファの基本的な使用法は前述したとおりですが,FileChannelクラスには他にも便利な機能があります。それをご紹介しましょう。

ファイルのマップ

FileChannelクラスには,ファイルのバイト列をそのままメモリーにマッピングする機能があります。マップした後はファイルをそのままByteBufferオブジェクトとして扱えます。

もちろん,マップできるからといって,どのようなサイズのファイルもマップできるわけではありません。マップ機能はメモリー・サイズやOSなどに依存するため,使用するには注意が必要です。また,マップするコストも高いので,ファイル・サイズが小さい場合,効果はありません。

具体的な使用法を見てみましょう。

チャネルの取得部分などは,FileCopier1クラスと同じなので,実際にファイルのマップをしている部分だけを抜粋して以下に示します。

    long size = srcChannel.size();
    long index = 0;
    while (index < size) {
        // マップするサイズを決める
        // サイズが大きい場合は複数回マップする
        int length;
        if (index + MAP_MAX < size) {
            length = MAP_MAX;
        } else {
            length = (int)(size - index);
        }
                  
        // ファイルのマップ
        // モードはRead Only
        ByteBuffer srcBuffer
            = srcChannel.map(FileChannel.MapMode.READ_ONLY,
                             index, length);
  
        // マップしたバッファを書き込み
        destChannel.write(srcBuffer);
		
        // index の更新
        index += length;
    }

ファイルのマップを行うにはFileChannel#mapメソッドを使用します。第1引数がマップのモードです。モードには三つあり,それぞれFileChannel.MapModeクラスで定数が定義されています。第2引数がマップの開始位置,第3引数がマップするサイズです。

このサンプルでは,ファイルのサイズがMAP_MAX以上の場合,複数回にわたってマップするようにしています。マップした後のバッファはposition = 0,limit = capacityになっています。

マップした後は普通のバッファとして扱えるので,そのまま書き込みを行います。

先々週のサンプルの実行結果からもわかるように,ファイルをマップした場合はとても高速に入出力を行うことが可能です。ただし,マップのコストは高いことをお忘れなく。

トランスファ

次の機能はチャネルのトランスファです。これはチャネルの出力を他のチャネルの入力に接続する機能です。また,他のチャネルの出力をチャネルの入力にすることも可能です。

この機能の使い方は簡単です。単にFileChannel#transferToメソッド,もしくはFileChannel#transferFromメソッドを使用するだけです。

トランスファを行っている部分を以下に示します。

    // チャネルのトランスファ
    // 読み込みのためのチャネルの出力を
    // 書き込みのためのチャネルの入力にトランスファする
    srcChannel.transferTo(0, srcChannel.size(), destChannel);

transferToメソッドの第1引数はトランスファを開始する位置,第2引数がトランスファするサイズです。第3引数がトランスファするチャネルで,型はWritableByteChannelインタフェースです。

えっ,これだけ,という感じですね。読み込み・書き込みの処理は全く記述する必要がありません。

こんなに簡単に書けて,しかも高速に入出力をしてくれるのですから,使わない手はありません。

今月のまとめ

New I/Oの存在は知っていても,何となく敬遠している方は多いのではないでしょうか。バイナリ・データであれば,チャネルもストリームを使っているのと大差ありません。しかも,パフォーマンスの向上も図れます。ぜひ試してみてください。

今月はファイルを扱いましたが,来月はNew I/Oでのソケットについて紹介します。特にノンブロッキングI/Oは強力な武器になるはずです。お楽しみに。

著者紹介 櫻庭祐一

横河電機の研究部門に勤務。同氏のJavaプログラマ向け情報ページ「Java in the Box」はあまりに有名