|
New I/O Channel 基礎編 |
|||||||||||||||||
チャネルってなんだろう? |
|||||||||||||||||
今回取り上げる Channel は、ファイルや Socket といった今までストリームとして扱っていたものに対するコネクションを表すインタフェースです。 今まで java.io パッケージで提供されていた InputStream/OutputStream やその派生クラス群があるのに、なぜ Channel を新たに導入したのでしょうか。 一番大きな理由はやはりパフォーマンスのようです。 ストリームだとちまちまと読み書きを行うイメージがありますが (あくまでもイメージです ^^;;)、Channel をインプリメントしたクラスではガバッと読んで、ガバッと書き込みを行うという感じです。まとめて行うので、その分効率が上がるというわけです。ここで活躍するのが前回の Buffer です。 実際には java.nio.channels.Channel インタフェースには読み込みや書き込みのメソッドは定義されておらず、Channel インタフェースから派生されたインタフェースで定義されています。Channel インタフェースとその派生インタフェースは図 1 のようになっています。 派生インタフェースは青で示してあります。 1 つずつ見ていきましょう。 まず、java.nio.channels.Channel インタフェースです。Channel インタフェースで定義されているのは close メソッドと isOpen メソッドです。Channel インタフェースをインプリメントしたクラスは基本的には自分から Channel オブジェクトに対してオープンはしませんし、new を使用してオブジェクトを生成することもしません。通常は、まずは stream があって、そこから Channel オブジェクトを取得するという方法をとります。 このため、Channel インタフェースには open メソッドは定義されていないのです。ただし、オープンしているかどうかを調べるために isOpen メソッドが用意されています。 オープンはしないのですが、close メソッドを使用したクローズは行います。 次は java.nio.channels.ReadableByteChannel インタフェースです。このインタフェースは読み込みのためのメソッドを定義しています。 read メソッドで読み込みを行いますが、このメソッドの引数は ByteBuffer オブジェクトになっており、読み込んだ結果はこのオブジェクトに格納されます。ByteBuffer クラスが使われていることで分かるように、このメソッドは完全に読み込みに徹しています。今までのストリーム系のクラスでは readLine メソッドや readInt メソッドなど、読み込んだときに何らかの処理を加えることがありました。これは使う側からすると便利でいいのですが、解析のための処理がはいるためパフォーマンスが落ちてしまうという欠点があります。 しかし、ReadableByteChannel インタフェースでは、とりあえず何も考えずに読み込み、読み込んだ後に何らかの処理が必要であればユーザが明示的にその処理を施すようになっています。 トータル的に見れば、逐次処理的なストリームに対して Channel を用いた方がパフォーマンスを向上させやすいことが分かると思います。 読み込みの次は書き込みです。 java.nio.channels.WritableByteChannel インタフェースは write メソッドが定義されています。ここでも ReadableByteChannel インタフェースと同様に write メソッドの引数は ByteBuffer オブジェクトになっています。 ByteChannel インタフェースは ReadableByteChannel インタフェースと WritableByteChannel インタフェースの両方をスーパークラスとして持ちます。ByteChannel インタフェース自体が定義しているメソッドはないので、単に読み書きのできる Channel という位置付けになります。 残りの 2 つのインタフェースも読み書きは異なりますが、基本的な考え方は同じです。 java.nio.channels.ScatteringByteChannel インタフェースで定義されている read メソッドは ByteBuffer オブジェクトの配列を引数としています。複数の ByteBuffer オブジェクトを用意しておいて、読めるだけ読み込んでしまおうというメソッドです。 java.nio.channels.GatheringByteChannel インタフェースは ScatteringByteChannel とは逆に書き込みを行う write メソッドを定義していますが、引数は同じく ByteBuffer オブジェクトの配列です。こちらも、複数の ByteBuffer オブジェクトをまとめて書き込みを行います。 これらの Channel インタフェースをはじめとするインタフェース群をインプリメントしているクラスは現状では次の 4 つです。
ストリームのように接続する先は何でもいいという汎用的なクラスはありません。それぞれ、ファイルにしろソケットにしろ、接続する対象にかなり依存したクラスになっています。汎用性という意味では劣るかもしれませんが、接続先に応じたチューニングや機能追加が行われているようです。 この他に、ストリームから Channel の生成、Channel からストリームの生成を行う java.nio.channels.Channels クラスというユーティリティクラスがあります。このクラスを使用して得られる Channel は FileChannel などとはことなり汎用的に使うことができます。 ここからは実際に Channel インタフェースをプログラム中に実際に使っていきたいのですが、まずは FileChannel クラスを例に Chnnel インタフェース群の基本的な機能や使い方を説明していきましょう。
|
基本的な機能 | |||||||||||||||||||||||||
Channel インタフェース群で定義されている read メソッドと write メソッドを中心に使ってみましょう。 まずは単純な読み書きを行うサンプルです。
ChannelTest1 クラスでは前述したように FileChannel クラスを使用しています。FileChannel クラスは Channel インタフェース群で定義されたメソッドはすべてインプリメントしているので、サンプルには好都合です。 まずは書き込みを行っている ChannelTest1 クラスの write メソッドから見ていきましょう。
FileChannel クラスは new を使用してオブジェクトを生成することはできません。その代わりに FileInputStream、FileOutputStream、RandomAceessFile クラスの getChannel メソッドを使用してオブジェクトを取得します。 書き込みには ByteBuffer オブジェクトを使用します。このサンプルでは文字列から String クラスの getBytes メソッドを使用してバイト列を生成し、それを ByteBuffer クラスの wrap メソッドを使用して ByteBuffer オブジェクトを生成しています。 赤で示したのが書き込みの部分です。書き込み内容を保持している ByteBuffer オブジェクト buffer を引数にしているだけです。 最後に FileChannel オブジェクトを close メソッドを使用してクローズします。 次に読み込みです。
FileChannel オブジェクトを得るところはほとんど同じです。その次に、読み込んだ内容を保持しておく ByteBuffer オブジェクトを生成しています。ByteBuffer オブジェクトの capacity は FileChannel クラスの size メソッドを使用して設定しています。 赤の部分が読み込みの部分です。読み込んだ結果が buffer に格納されます。 読み込んだ後は、buffer の position が最後の読み込んだデータの後に移動しているので注意が必要です。buffer のはじめからデータを取り出すのであれば ByteBuffer クラスの clear メソッドか flip メソッドで position を 0 に移動させておく必要があります。 その後の部分で、読み込んだバイト列を出力しています。 最後は忘れずにクローズを行いましょう。 それでは実行してみましょう。引数に読み書きを行うファイル名を指定して実行します。
実行すると引数で指定したファイルができているはずです。result.txt の中身は次のようになりました。
出力された内容と同一になっていることが分かります。 このサンプルでは一度に書き込み/読み込みを行いましたが、複数回に分けて行ってももちろんかまいません。
ChannelTest2 クラスの write メソッドを次に示します。
文字列の配列 texts を複数回に分けて書き込みを行っています。また、読み込みは
capacity が 10 の ByteBuffer オブジェクトを使用して複数回に渡って読み込みを繰りかえしています。 FileChannel クラスの read メソッドは読み込んだバイト数が戻り値なので、それを累計してファイルのサイズと比較することで読み込みのループを抜け出すようにしました。 また、read メソッドの戻り値が 0 もしくは -1 であればファイルの最後なので、そこまで読み込むという方法でもいいかもしれません。 実行結果は次のようになりました。
生成したファイルは
ChannelTest2 クラスでは複数回で読み込み/書き込みを行いましたが、もっと便利なものがあることを忘れていました。ScatteringByteChannel インタフェースと GatheringByteChannel インタフェースで定義されている read メソッド、write メソッドです。この 2 つのメソッドは引数が ByteBuffer オブジェクトの配列になっているので、一度に読み込み/書き込みが行えます。
書き込みは ByteBuffer オブジェクトの配列を準備してから行います。
この例で分かるように ByteBuffer オブジェクトの capacity は同じでなくてもかまいません。 読み込みの方は
適当に capacity が 10 の ByteBuffer オブジェクトを 10 個使って読み込みを行ってみました。 実行してみると
このサンプルでは読み込みように準備した ByteBuffer オブジェクトの配列で全てのデータの読み込みができました。しかし、ByteBuffer オブジェクトの配列がデータ量に足りなくても、何度でも read メソッドは使用できるので安心してください。 ScatteringByteBuffer インタフェースと GatheringByteBuffer インタフェースにはもう 1 つづつメソッドが定義されています。やはり、read メソッドと write メソッドですが、引数に offset と length が指定できます。 この offset と length が何を意味しているのでしょうか。それを調べて見ましょう。
同じように書き込みから
ChannelTest3 クラスと異なるのは赤で示した部分だけです。offset が 1、length が 2 です。これを実行してできたファイルは
これを見ると offset と length の意味がわかります。 ようするに write メソッドで書き込むのが buffers[offset] から length だけ (buffers[offset + length - 1] まで) だということです。 読み込みも同じようになります。
offset が 2、length が 3 になっています。実行すると
結局、読み込んだ結果が buffers[offset] から length 分だけになるということでした。
|
例外に気をつけよう |
||||||||||||||||
ReadableByteChannel インタフェースや WritableByteChannel などで定義された read メソッド、write メソッドは InputStream クラスの read メソッドなどと同様に IOException を投げます。 IOException 以外にも java.nio.channels パッケージで新たに定義された例外が発生します。
NonReadableChannelException は Channel がまだオープンしないとき read/write を行うと発生します。同じように ClosedChannelException は Channel がクローズされているのに read/write を行うと発生します。 AsynchronousCloseException は read/write を行っているときに、他のスレッドで Channel オブジェクトをクローズしたときに発生します。 図 2 に示したように、Thread オブジェクト a が Channel オブジェクトの read メソッドをコールしているとき、他の Thread オブジェクト b が Channel オブジェクトに対して close メソッドをコールすると AsynchrounsCloseException が発生します。図 2 では read メソッドですが、write メソッドでも同様です。
CloseByInterruptException は図 3 に示したように、read/write を行っているスレッドを他のスレッドが interrupt したときに発生します。
この 2 つの例外は、マルチスレッドで Channel を使用するときに注意が必要です。
|
較べてみよう |
|||||||||||||
せっかくなので、Channel を使った場合とストリームを使った場合の比較をしてみることにしましょう。簡単なサンプルとしてファイルのコピーを従来のストリームを使用したものと、Channel を利用したものを作ってみました。
実行するには第 1 引数でストリームか Channel の指定を行います。-st ならばストリーム、-ch なら Channel です。第 2 引数にコピー元のファイル名、第 3 引数にコピー先のファイル名を指定します。 実行すると次のようになります。大きいファイルの方が差が出やすいと思ったので、Java 2 SDK をコピーしてみました。Java 2 SDK v1.4 beta 2 は 37.2MB あります。
Channel を使ったものは 1.832 秒、ストリームを使ったものが 2.478 秒かかっています。 10 回行って平均すると次表のようになりました。Java 2 SDK は v1.4 beta 2 を使用しました。
結構違いがでるものです。もっとも、バッファのサイズなどをちゃんとチューニングをすると結果が変わるかもしれませんけど... コピーを行っている部分を比較してみましょう。まずはストリームを使ったファイルのコピーは
Channel を使用したほうは
ここでは、よく使えるイディオムを 2 つ使いました。 1 つめが ByteBuffer オブジェクトに DirectByteBuffer オブジェクトを使用しているところです。ダイレクトとノーマルではパフォーマンスがかなり違います。 2 つめが読み込みと書き込みの間に ByteBuffer オブジェクトを flip しているところです。write メソッドは ByteBuffer オブジェクトの position から limit までの要素を書き込みます。一方の read メソッドを使うと ByteBuffer オブジェクトに要素を書き込んだところまで position が移動しますから、その position を先頭に移す必要があります。 このとき、通常であれば clear メソッドを使用すると思います。clear メソッドでは position が 0、limit が capactity になります。 これだと、ファイルの最後などで read メソッドで読み込んだバイト数が ByteBuffer オブジェクトの capacity 未満だったときに、どこまで書き込みを行うかを覚えておく必要があります。 これに対して flip メソッドを使うと、position は 0 になりますが、limit はもともとの position になるので、なにも考えずに write することができます。
|
Channel とストリームの橋渡し |
||||||||||
今までのサンプルはみな FileChannel オブジェクトを直接 FileInputStream/FileOutputStream オブジェクトから取得していました。しかし、単なる InputStream/OutputStream オブジェクトから Channel オブジェクトを得ることはできないのでしょうか。 このような用途に使用できるのが、java.nio.channels.Channels クラスです。 Channels クラスでは次の 3 種類の機能があります。
Reader/Writer にも変換できますし、文字コードの指定も可能です。
|
最後に |
||
長くなってしまったので、FileChannel クラスなどの固有の機能は次回以降にまわすことにしましょう。 今回使用したサンプルはここからダウンロードできます。 参考 URL
(Sep. 2001)
|
|