Go to Previous Page Go to Contents Go to Java Page Go to Next Page
New Features of Java2 SDK, Standard Edition, v1.4
 
 

New I/O Channel 基礎編

 
 

チャネルってなんだろう?

 
 

今回取り上げる Channel は、ファイルや Socket といった今までストリームとして扱っていたものに対するコネクションを表すインタフェースです。

今まで java.io パッケージで提供されていた InputStream/OutputStream やその派生クラス群があるのに、なぜ Channel を新たに導入したのでしょうか。

一番大きな理由はやはりパフォーマンスのようです。

ストリームだとちまちまと読み書きを行うイメージがありますが (あくまでもイメージです ^^;;)、Channel をインプリメントしたクラスではガバッと読んで、ガバッと書き込みを行うという感じです。まとめて行うので、その分効率が上がるというわけです。ここで活躍するのが前回の Buffer です。

実際には java.nio.channels.Channel インタフェースには読み込みや書き込みのメソッドは定義されておらず、Channel インタフェースから派生されたインタフェースで定義されています。Channel インタフェースとその派生インタフェースは図 1 のようになっています。

Channel インタフェース群
図 1 Buffer クラスのクラス図

派生インタフェースは青で示してあります。

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 つです。

クラス名 インプリメントしたインタフェース 説明
FileChannel ByteChannel, ScatteringByteChannel, GatheringByteChannel ファイルに対する Channel
SocketChannel ByteChannel, ScatteringByteChannel, GatheringByteChannel Socket クラスに対応する Channel
ServerSocketChannel Channel ServerSocket クラスに対応する Channel
DatagramChannel ByteChannel, ScatteringByteChannel, GatheringByteChannel DatagramSocket クラスに対応する Channel

ストリームのように接続する先は何でもいいという汎用的なクラスはありません。それぞれ、ファイルにしろソケットにしろ、接続する対象にかなり依存したクラスになっています。汎用性という意味では劣るかもしれませんが、接続先に応じたチューニングや機能追加が行われているようです。

この他に、ストリームから Channel の生成、Channel からストリームの生成を行う java.nio.channels.Channels クラスというユーティリティクラスがあります。このクラスを使用して得られる Channel は FileChannel などとはことなり汎用的に使うことができます。

ここからは実際に Channel インタフェースをプログラム中に実際に使っていきたいのですが、まずは FileChannel クラスを例に Chnnel インタフェース群の基本的な機能や使い方を説明していきましょう。

 

 
  基本的な機能  
 

Channel インタフェース群で定義されている read メソッドと write メソッドを中心に使ってみましょう。

まずは単純な読み書きを行うサンプルです。

アプリケーションのソース ChannelTest1.java

ChannelTest1 クラスでは前述したように FileChannel クラスを使用しています。FileChannel クラスは Channel インタフェース群で定義されたメソッドはすべてインプリメントしているので、サンプルには好都合です。

まずは書き込みを行っている ChannelTest1 クラスの write メソッドから見ていきましょう。

    private String text = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789あいうえお";
 
    public void write(String filename){
        try{
            FileOutputStream stream = new FileOutputStream(filename);
            FileChannel channel = stream.getChannel();
         
            byte[] bytes = text.getBytes();
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
 
            channel.write(buffer);
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

FileChannel クラスは new を使用してオブジェクトを生成することはできません。その代わりに FileInputStream、FileOutputStream、RandomAceessFile クラスの getChannel メソッドを使用してオブジェクトを取得します。

書き込みには ByteBuffer オブジェクトを使用します。このサンプルでは文字列から String クラスの getBytes メソッドを使用してバイト列を生成し、それを ByteBuffer クラスの wrap メソッドを使用して ByteBuffer オブジェクトを生成しています。

赤で示したのが書き込みの部分です。書き込み内容を保持している ByteBuffer オブジェクト buffer を引数にしているだけです。

最後に FileChannel オブジェクトを close メソッドを使用してクローズします。

次に読み込みです。

    public void read(String filename){
        try{
            FileInputStream stream = new FileInputStream(filename);
            FileChannel channel = stream.getChannel();
        
            ByteBuffer buffer = ByteBuffer.allocate((int)channel.size());
 
            channel.read(buffer);
 
            buffer.clear();
            byte[] bytes = new byte[buffer.capacity()];
            buffer.get(bytes);
            System.out.println("Buffer: " + new String(bytes));
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

FileChannel オブジェクトを得るところはほとんど同じです。その次に、読み込んだ内容を保持しておく ByteBuffer オブジェクトを生成しています。ByteBuffer オブジェクトの capacity は FileChannel クラスの size メソッドを使用して設定しています。

赤の部分が読み込みの部分です。読み込んだ結果が buffer に格納されます。

読み込んだ後は、buffer の position が最後の読み込んだデータの後に移動しているので注意が必要です。buffer のはじめからデータを取り出すのであれば ByteBuffer クラスの clear メソッドか flip メソッドで position を 0 に移動させておく必要があります。

その後の部分で、読み込んだバイト列を出力しています。

最後は忘れずにクローズを行いましょう。

それでは実行してみましょう。引数に読み書きを行うファイル名を指定して実行します。

C:\temp>java ChannelTest1 result.txt
Buffer: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789あいうえお

実行すると引数で指定したファイルができているはずです。result.txt の中身は次のようになりました。

ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789あいうえお

出力された内容と同一になっていることが分かります。

このサンプルでは一度に書き込み/読み込みを行いましたが、複数回に分けて行ってももちろんかまいません。

アプリケーションのソース ChannelTest2.java

ChannelTest2 クラスの write メソッドを次に示します。 

    private String[] texts = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
                              "abcdefghijklmnopqrstuvwxyz",
                              "0123456789",
                              "あいうえお"};
 
    public ChannelTest2(String filename) {
        write(filename);
        read(filename);
    }
 
    public void write(String filename){
        try{
            FileOutputStream stream = new FileOutputStream(filename);
            FileChannel channel = stream.getChannel();
        
            for (int i = 0 ; i < texts.length ; i++) {
                ByteBuffer buffer = ByteBuffer.wrap(texts[i].getBytes());

                channel.write(buffer);
            }
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

文字列の配列 texts を複数回に分けて書き込みを行っています。また、読み込みは

    public void read(String filename){
        try{
            FileInputStream stream = new FileInputStream(filename);
            FileChannel channel = stream.getChannel();
        
            int sizeOfReadingBytes = 0;
 
            while (sizeOfReadingBytes < channel.size()){
                ByteBuffer buffer = ByteBuffer.allocate(10);
                buffer.clear();
 
                sizeOfReadingBytes += channel.read(buffer);
                 
                buffer.clear();
                byte[] bytes = new byte[buffer.capacity()];
                buffer.get(bytes);
                System.out.println("Buffer: " + new String(bytes));
            }
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

capacity が 10 の ByteBuffer オブジェクトを使用して複数回に渡って読み込みを繰りかえしています。

FileChannel クラスの read メソッドは読み込んだバイト数が戻り値なので、それを累計してファイルのサイズと比較することで読み込みのループを抜け出すようにしました。

また、read メソッドの戻り値が 0 もしくは -1 であればファイルの最後なので、そこまで読み込むという方法でもいいかもしれません。

実行結果は次のようになりました。

C:\temp>java ChannelTest2 result.txt
Buffer: ABCDEFGHIJ
Buffer: KLMNOPQRST
Buffer: UVWXYZabcd
Buffer: efghijklmn
Buffer: opqrstuvwx
Buffer: yz01234567
Buffer: 89あいうえ
Buffer: お

生成したファイルは

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789あいうえお

ChannelTest2 クラスでは複数回で読み込み/書き込みを行いましたが、もっと便利なものがあることを忘れていました。ScatteringByteChannel インタフェースと GatheringByteChannel インタフェースで定義されている read メソッド、write メソッドです。この 2 つのメソッドは引数が ByteBuffer オブジェクトの配列になっているので、一度に読み込み/書き込みが行えます。

アプリケーションのソース ChannelTest3.java

書き込みは ByteBuffer オブジェクトの配列を準備してから行います。

    private String[] texts = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
                              "abcdefghijklmnopqrstuvwxyz",
                              "0123456789",
                              "あいうえお"};
 
    public void write(String filename){
        try{
            FileOutputStream stream = new FileOutputStream(filename);
            FileChannel channel = stream.getChannel();
        
            ByteBuffer[] buffers = new ByteBuffer[texts.length];
            for(int i = 0 ; i < texts.length ; i++){
                buffers[i] = ByteBuffer.wrap(texts[i].getBytes());
            }
 
            channel.write(buffers);
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

この例で分かるように ByteBuffer オブジェクトの capacity は同じでなくてもかまいません。

読み込みの方は

    public void read(String filename){
        try{
            FileInputStream stream = new FileInputStream(filename);
            FileChannel channel = stream.getChannel();
        
            ByteBuffer[] buffers = new ByteBuffer[10];
            for (int i = 0 ; i < 10 ; i++){
                buffers[i] = ByteBuffer.allocate(10);
            }
 
            channel.read(buffers);
 
            for (int i = 0 ; i < 10 ; i++) {
                buffers[i].clear();
                byte[] bytes = new byte[buffers[i].capacity()];
                buffers[i].get(bytes);
                System.out.println("buf[" + i + "]: " + new String(bytes));
            }
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

適当に capacity が 10 の ByteBuffer オブジェクトを 10 個使って読み込みを行ってみました。

実行してみると

C:\temp>java ChannelTest3 result.txt
buf[0]: ABCDEFGHIJ
buf[1]: KLMNOPQRST
buf[2]: UVWXYZabcd
buf[3]: efghijklmn
buf[4]: opqrstuvwx
buf[5]: yz01234567
buf[6]: 89あいうえ
buf[7]: お
buf[8]: 
buf[9]: 

このサンプルでは読み込みように準備した ByteBuffer オブジェクトの配列で全てのデータの読み込みができました。しかし、ByteBuffer オブジェクトの配列がデータ量に足りなくても、何度でも read メソッドは使用できるので安心してください。

ScatteringByteBuffer インタフェースと GatheringByteBuffer インタフェースにはもう 1 つづつメソッドが定義されています。やはり、read メソッドと write メソッドですが、引数に offset と length が指定できます。

この offset と length が何を意味しているのでしょうか。それを調べて見ましょう。

アプリケーションのソース ChannelTest4.java

同じように書き込みから

    private String[] texts = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
                              "abcdefghijklmnopqrstuvwxyz",
                              "0123456789",
                              "あいうえお"};
 
    public void write(String filename){
        try{
            FileOutputStream stream = new FileOutputStream(filename);
            FileChannel channel = stream.getChannel();
        
            ByteBuffer[] buffers = new ByteBuffer[texts.length];
            for(int i = 0 ; i < texts.length ; i++){
                buffers[i] = ByteBuffer.wrap(texts[i].getBytes());
            }
 
            channel.write(buffers, 1, 2);
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

ChannelTest3 クラスと異なるのは赤で示した部分だけです。offset が 1、length が 2 です。これを実行してできたファイルは

abcdefghijklmnopqrstuvwxyz0123456789

これを見ると offset と length の意味がわかります。

ようするに write メソッドで書き込むのが buffers[offset] から length だけ (buffers[offset + length - 1] まで) だということです。

読み込みも同じようになります。

    public void read(String filename){
        try{
            FileInputStream stream = new FileInputStream(filename);
            FileChannel channel = stream.getChannel();
        
            ByteBuffer[] buffers = new ByteBuffer[10];
            for (int i = 0 ; i < 10 ; i++){
                buffers[i] = ByteBuffer.allocate(10);
            }
 
            channel.read(buffers, 2, 3);
 
            for (int i = 0 ; i < 10 ; i++) {
                buffers[i].clear();
                byte[] bytes = new byte[buffers[i].capacity()];
                buffers[i].get(bytes);
                System.out.println("buf[" + i + "]: " + new String(bytes));
            }
 
            channel.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

offset が 2、length が 3 になっています。実行すると

C:\temp>java ChannelTest4 result.txt
buf[0]: 
buf[1]: 
buf[2]: abcdefghij
buf[3]: klmnopqrst
buf[4]: uvwxyz0123
buf[5]: 
buf[6]: 
buf[7]: 
buf[8]: 
buf[9]: 

結局、読み込んだ結果が buffers[offset] から length 分だけになるということでした。

 

 
 

例外に気をつけよう

 
 

ReadableByteChannel インタフェースや WritableByteChannel などで定義された read メソッド、write メソッドは InputStream クラスの read メソッドなどと同様に IOException を投げます。

IOException 以外にも java.nio.channels パッケージで新たに定義された例外が発生します。

例外クラス 説明
NonReadableChannelException Channel がオープンしていない
ClosedChannelException Channel がクローズしている
AsynchronousCloseException read や write を行っているスレッドとは別のスレッドで Channel がクローズされた
ClosedByInterruptException read や write を行っているスレッドとは別のスレッドが interrupt された

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 メソッドでも同様です。

AsynchrousCloseException
図 2 AsynchronusCloseExcpetion

 

CloseByInterruptException は図 3 に示したように、read/write を行っているスレッドを他のスレッドが interrupt したときに発生します。

CloseByInterruptException
図 3 CloseByInterruptExcpetion

この 2 つの例外は、マルチスレッドで Channel を使用するときに注意が必要です。

 

 
 

較べてみよう

 
 

せっかくなので、Channel を使った場合とストリームを使った場合の比較をしてみることにしましょう。簡単なサンプルとしてファイルのコピーを従来のストリームを使用したものと、Channel を利用したものを作ってみました。

アプリケーションのソース ChannelTest5.java

実行するには第 1 引数でストリームか Channel の指定を行います。-st ならばストリーム、-ch なら Channel です。第 2 引数にコピー元のファイル名、第 3 引数にコピー先のファイル名を指定します。

実行すると次のようになります。大きいファイルの方が差が出やすいと思ったので、Java 2 SDK をコピーしてみました。Java 2 SDK v1.4 beta 2 は 37.2MB あります。

C:\temp>java ChannelTest5 -ch j2sdk-1_4_0-beta2-win.exe j2sdk1.4.exe
Copy by Channel
Total time = 1832
 
C:\temp>java ChannelTest5 -st j2sdk-1_4_0-beta2-win.exe j2sdk1.4.exe
Copy by Stream
Total time = 2473

Channel を使ったものは 1.832 秒、ストリームを使ったものが 2.478 秒かかっています。

10 回行って平均すると次表のようになりました。Java 2 SDK は v1.4 beta 2 を使用しました。

方法 処理時間 [秒]
Channel
1828.6
ストリーム
2631.8
Athron 800MHz, Memory 512MB, Win2000 SP2 を使用

結構違いがでるものです。もっとも、バッファのサイズなどをちゃんとチューニングをすると結果が変わるかもしれませんけど...

コピーを行っている部分を比較してみましょう。まずはストリームを使ったファイルのコピーは

    public void copyFileByStream(String srcFilename, String destFilename){
        try{
            InputStream in = new FileInputStream(srcFilename);
            OutputStream out = new FileOutputStream(destFilename);
        
            long start = System.currentTimeMillis();
 
            byte[] buffer = new byte[10000000];
            int n;
            while ((n = in.read(buffer)) != -1) {
                out.write(buffer, 0, n);
            }
 
            out.flush();
 
            long end = System.currentTimeMillis();
 
            System.out.println("Total time = " + (end-start));
            out.close();
            in.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

Channel を使用したほうは

    public void copyFileByChannel(String srcFilename, String destFilename){
        try{
            FileChannel in = (new FileInputStream(srcFilename)).getChannel();
            FileChannel out = (new FileOutputStream(destFilename)).getChannel();
        
            long start = System.currentTimeMillis();
 
            ByteBuffer buffer = ByteBuffer.allocateDirect(10000000);
 
            while (true) {
                buffer.clear();
                if(in.read(buffer) < 0){
                    break;
                }
                buffer.flip();
                out.write(buffer);
            }
 
            long end = System.currentTimeMillis();
 
            System.out.println("Total time = " + (end-start));
 
            out.close();
            in.close();
        }catch(FileNotFoundException ex){
            ex.printStackTrace();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }

ここでは、よく使えるイディオムを 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 種類の機能があります。

機能 メソッド
ストリームから Channel の取得
  • ReadableByteChannel newChannel(InputStream in)
  • WritableByteChannel newChannel(OutputStream out
Channel からストリームの取得
  • InputStream newInputStream(ReadableByteChannel ch)
  • OutputStream newOutputStream(WritableByteChannel out)
Channel から Reader/Writer の取得
  • Reader newReader(ReadableByteChannel ch, CharsetDecoder dec, int MinBufferCap)
  • Reader newReader(ReadableByteChannel ch, String csName)
  • Writer newWriter(WritableByteChannel ch, CharsetEncoder enc, int MinBufferCap)
  • Writer newWriter(WritableByteChannel ch, String csName)


Channels クラスを使用すれば、汎用的にストリームと Channel の変換ができます。例えば、FileInputStream/FileOutputStream 以外にも意味があるかどうかはさておいて ByteArrayInputStream や StringBufferInputStream などからも Channel を得ることができます。

Reader/Writer にも変換できますし、文字コードの指定も可能です。

 

 
 

最後に

 
 

長くなってしまったので、FileChannel クラスなどの固有の機能は次回以降にまわすことにしましょう。

今回使用したサンプルはここからダウンロードできます。

参考 URL

(Sep. 2001)

 

 
 
Go to Previous Page Go to Contents Go to Java Page Go to Next Page