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 Buffer

 
 

配列とはちょっと違う

 
 

New I/O の一番手は Buffer です。

Buffer は boolean を除いたプリミティブに特化したコンテナクラスです。ただし、List や Vector などとは異なり、サイズを変更することはできません。

と、聞くと、「なんだ配列と変わらないじゃないか」と思いませんか。逆に、オブジェクトを扱えない分、配列より劣っているような感じも受けます。

でも、実際にはそんなことはなくて、いろいろと使える場面もあると思います。Buffer の主な特徴は

  1. プリミティブに特化したコンテナ
  2. 基本的にはシーケンシャルなアクセス
  3. アクセスしている地点を変更するためのメソッド群
  4. 使用するプリミティブ型ごとに Buffer クラスの派生クラスが用意されている
  5. 読み取りは get メソッド、書き込みは put メソッドという統一したアクセス法
  6. Java のヒープ外のメモリへの直接アクセスをサポート
  7. メモリにマップされたファイルへのアクセスをサポート

などがあります。

Buffer オブジェクトにアクセスするときには基本的にシーケンシャルアクセスなので (ランダムアクセスでも使用可能ですが)、ArrayList クラスなどに比較すると高速にアクセスを行うことができます。ただし、読み込み/書き込みを行っている地点を変更するためにさまざまなメソッドが用意されており、必ずしも一度終わりまでアクセスしたらそこでおしまいというわけではありません。

Buffer クラスはプリミティブに特化しているので、例えば get メソッドなどの戻り値を Object にして、派生クラスはこれをオーバライドすることができません。したがって、Buffer クラスには get メソッドなどは定義されていません。get メソッドは派生クラスで扱うプリミティブ専用に定義されます。

これは逆にいえば、プリミティブの型の数だけ Buffer の派生クラスがあることになります。ただし、Buffer クラスでは get メソッドなどは定義されていなくても、すべての派生クラスでアクセスの方法を統一しています。このため、1 つのクラスの使い方が分かれば、他の派生クラスも同じように使用することができます。

最後の 2 つの特徴が特に New I/O での売りになる特徴になると筆者は考えています。特にヒープ外のメモリアクセスは今までは JNI を使用して行うしかなかったので、大きな特徴になると思います。これで、大きなデータでも高速に効率よくアクセスすることが可能になります。ただし、ヒープ外なので取り扱いには注意する必要があると思いますが。

ファイルをメモリにマップするということは、例えばファイルのある部分が頻繁にアクセスされていた場合、その部分をメモリに読み込んでしまってそれに対してアクセスするようにすることです。いわゆる、ファイルのメモリキャッシュですね。

このようにメモリにマップされたファイルをアクセスするための専用の Buffer クラスの派生クラスが用意されているので、通常のファイルアクセスより高速にアクセスを行うことができます。

このように見ていくと、やはり Buffer クラスは Channel などと組み合わせることで大規模なデータを効率よくアクセスするためにデザインされているようです。

さて、Buffer クラスの派生クラスはプリミティブごとにあるということを説明しましたが、実際にはどんな感じなんでしょうか。Buffer クラス群のクラス図を図 1 に示します。

Buffer クラス群
図 1 Buffer クラスのクラス図

Buffer クラスは派生クラスとして、ByteBuffer などのプリミティブ型に対応した Buffer クラスが用意されているのが分かると思います。図 1 の青で表したクラスがそれです。また、メモリにマップしたファイルを扱うための MappedByteBuffer クラスがあります。

ところが、これらのクラスはすべて abstract クラスになっています。というのも、Buffer クラスの実装にはヒープを使用して行う実装と、ヒープの外に作る方法の 2 種類があるからです。最も基本的な byte 型に関してだけ実装クラスを示しました。図中では赤で示されているクラスです。これらのクラスはプライベートなクラスになっています。

ヒープを利用して Buffer を実装しているのが HeapByteBuffer クラスです。その派生クラスである HeapByteBufferR クラスは書き込みが禁止されているリードオンリーのクラスです。

ヒープ外のメモリにアクセスするのが DirectByteBuffer クラスです。同じようにリードオンリーの DirectByteBufferR クラスも用意されています。

クラス構成も分かったので、次からは Buffer クラスの使い方を調べていきましょう。

 

 
  位置について  
 

使い方を調べるにあたって、いくつかのステップに分けましょう。

  • Step 1 Buffer クラスの基本的な使い方
  • Step 2 プリミティブに特化した派生クラスに共通の機能の使い方
  • Step 3 それぞれのクラス特有の機能の使い方

さっそく、Buffer クラスからいきましょう。

Buffer クラスには前述したように、要素にアクセスするためのメソッドは定義されていません。それでは、何が Buffer クラスに残されているのでしょうか。その答えは、位置です。

と、いわれてもさっぱり分からないですよね。Buffer クラスでは基本的に要素に対してシーケンシャルなアクセスを行います。このため、どこまでアクセスしたかを示すための位置 position を常に保持しています。

この position 以外にも、Buffer オブジェクトのコンテナのサイズを保持する capacity、position の限界値を示す limit があります。もう 1 つ、それほど使わないとは思いますが、position の記憶させておくための mark というのもあります。この 4 つのプロパティに関する操作が Buffer クラスには用意されています。

名前 説明
position 要素へのアクセスを行った位置を示す
limit position の最大値を示す
capacity コンテナのサイズ
mark position を記憶させておいたもの

これら 4 つのプロパティには次のような式が常に成り立ちます。

0 <= mark <= position <= limit <= capacity

ただし、mark は常にあるとは限りません。明示的に設定を行わない限り mark の値はないのです。

例をあげてみましょう。capacity が 15 の Buffer オブジェクトは次のようになります。

Buffer の例

矢印で際しているところが各プロパティの位置なので、この Buffer オブジェクトのプロパティは

  • position = 2
  • limit = 10
  • capacity = 15

になっています。

それでは、さっそくサンプルでこれらのプロパティを操作してみましょう。

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

BufferTest1.java がメインになるのですが、Buffer オブジェクトの表示などを行うユーティリティクラス ByteBufferUtility.java も作りました。今回はすべてのサンプルでこのクラスを使用します。ここでは、特に ByteBufferUtility クラスの説明はしません。このドキュメントをある程度読んでいただければ、ByteBufferUtility クラスもすぐに理解できると思います。

それでは、BufferTest1 クラスの説明をしていきましょう。

BufferTest1 クラスでは ByteBuffer オブジェクトを使用して、position などのプロパティを操作しています。これらの操作は Buffer クラスで定義してあるものなので、ByteBuffer クラス以外のクラスを使用しても OK です。

Buffer オブジェクトの生成には allocate メソッドを使用します。引数は Buffer オブジェクトのサイズです。

        ByteBuffer buffer = ByteBuffer.allocate(15);
        
        ByteBufferUtility.initByteBuffer(buffer);    // 値の設定
        ByteBufferUtility.printByteBuffer(buffer);   // 表示

allocate メソッドは static なメソッドなので、オブジェクトがなくてもコールすることができます。ただし、このメソッドは Buffer クラスで定義されていないので、使用したい派生クラスの allocate メソッドを使用するようにします。ここでは、サイズが 15 の ByteBuffer オブジェクトを生成しています。

次の行で生成した Buffer オブジェクトに適当に値を代入しています。また、その次の行でこの Buffer オブジェクトの表示を行っています。

ここまでの部分を実行したときにはこんな感じになります。

C:\temp>java BufferTest1
P                                                           LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

P, L, C はそれぞれ position, limit, capacity の位置を表しています。初期状態では position = 0, limit = 15, capacity = 15 になっています。

[ ] で囲まれているのが、Buffer オブジェクトが保持している要素になります。

次に、position を動かしてみましょう。これには position メソッドを使用します。int の引数がある場合は position をその位置に移動させ、引数無しの場合は現在の position の位置を返します。

        // position の移動
        System.out.println("\nposition を 12 へ移動");
        buffer.position(12);
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println("\nposition を 5 へ移動");
        buffer.position(5);
        ByteBufferUtility.printByteBuffer(buffer);

結果は

position を 12 へ移動
                                                P           LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

position を 5 へ移動
                    P                                       LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

limit を移動させるには、limit メソッドを使います。position メソッドと同様に、引数ありで位置の設定、引数無しで limit の位置を返します。

        // limit の移動
        System.out.println("\nlimit を 10 へ移動");
        buffer.limit(10);
        ByteBufferUtility.printByteBuffer(buffer);

出力を次に示します。

limit を 10 へ移動
                    P                   L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

capacity は不変なので、これを設定することはできません。ただし、capacity メソッドはあって、引数無しで capacity を返します。

ところで、position, limit, capacity の関係は上述したように position <= limit <= capacity ですが、これが守られなかったらどうなるでしょう。さっそく、やってみましょう。

        // position を limit より大きくすると
        System.out.println("\nposition を 12 へ移動 (position > limit)");
        buffer.position(12);
        ByteBufferUtility.printByteBuffer(buffer);

出力結果は

position を 12 へ移動 (position > limit)
java.lang.IllegalArgumentException
        at java.nio.Buffer.position(Buffer.java:208)
        at BufferTest1.<init>(BufferTest1.java:27)
        at BufferTest1.main(BufferTest1.java:63)

まぁ、あたりまえですが例外 (IllegalArgumentException) が発生しました。IllegalArgumentException は Runtime Exception なので、catch を行う必要はないのですが、例外が発生した後に position がどうなっているかを調べるために catch をしてみましょう。

        // position を limit より大きくすると
        System.out.println("\nposition を 12 へ移動 (position > limit)");
        try {
            buffer.position(12);
        } catch (IllegalArgumentException ex) {
            ex.printStackTrace();
        }
        ByteBufferUtility.printByteBuffer(buffer);

position を 12 へ移動 (position > limit)
java.lang.IllegalArgumentException
        at java.nio.Buffer.position(Buffer.java:208)
        at BufferTest1.<init>(BufferTest1.java:28)
        at BufferTest1.main(BufferTest1.java:63)
                    P                   L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

例外が起きても、position の位置は変わりませんでした。

同様に limit を position 以下にしたり、position, limit を capacity 以上にした場合にも IllegalArgumentException が発生します。

position メソッドなど以外にも、postion などを操作することができます。次のサンプルではそのような操作をまとめてみました。

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

Clear

始めに登場するのは clear です。clear は position, limit を初期状態に戻します。つまり、postion = 0, limit = capacity に成ります。ただし、保持している要素は一切変更はありません。

Buffer clear

 

        // Clear 
        System.out.println("\nBuffer Clear");
        buffer.position(7);
        buffer.limit(10);
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println();

        buffer.clear();
        ByteBufferUtility.printByteBuffer(buffer);

Buffer Clear
                            P           L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

P                                                           LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

 

Rewind

rewind は position だけを 0 に移動させます。 position(0) と同じ結果になります。

Buffer rewind

 

        // Rewind
        System.out.println("\nBuffer Rewind");
        buffer.position(7);
        buffer.limit(10);
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println();

        buffer.rewind();
        ByteBufferUtility.printByteBuffer(buffer);

Buffer Rewind
                            P           L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

P                                       L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

 

Flip

flip は position と limit を入れ替えるのですが、position は limit より大きくなることはないので、実際には limit を position の位置にして、position は 0 になります。

Buffer flip

 

        // Flip
        System.out.println("\nBuffer Flip");
        buffer.position(7);
        buffer.limit(10);
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println();

        buffer.flip();
        ByteBufferUtility.printByteBuffer(buffer);

Buffer Flip
                            P           L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

P                           L                               C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

 

Mark & Reset

最後が mark と reset です。プロパティの mark はこのときだけ使用されます。

mark メソッドは現在の postion の位置を記憶しておきます。そして、その後 position が移動したときに reset メソッドをコールすると、mark したところに position が移動します。

Buffer mark & reset

 

        // Mark and Reset
        System.out.println("\nBuffer Mark & Reset");
        buffer.position(2);
        buffer.mark();         // position の位置に mark
        
        buffer.position(7);
        buffer.limit(10);
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println();

        buffer.reset();
        ByteBufferUtility.printByteBuffer(buffer);

Buffer Mark & Reset
                            P           L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

        P                               L                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

 

 
 

要素へのアクセス 読み込み編

 
 

次のステップはプリミティブに特化した派生クラスに共通の機能の使い方ということで、要素へのアクセスを試みましょう。

要素へのアクセスには 2 種類の方法があります。

相対アクセス position の位置から読み込み/書き込みを行う。position は最後に読み込み/書き込みが行われた位置に移動する。
絶対アクセス 読み込み/書き込みの開始位置を指定して行う。position の位置は変化しない。

 

相対・絶対アクセスとも読み込みは get メソッドを使用します。相対・絶対の区別はメソッドの引数によって決まります。例として ByteBuffer クラスのメソッドを示しますが、他のクラスでも引数や戻り値の型が異なるだけで、使い方は同じです。

メソッド名 相対/絶対 説明
byte get() 相対 position 位置の 1 要素の読み込み
byte get(byte[] dst) 相対 position 位置から dst のサイズだけ読み込み
byte get(byte[] dst, int offset, int length)
相対 position の位置から length バイトの読み込むが、dst には offset から書き込む
byte get(int index) 絶対 index の位置の要素の読み込み

さっそく、サンプルで見ていきましょう。

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

始めは単純な get() メソッドからです。get メソッドをコールすると position が 1 つ進みます。

Buffer get()

 

        System.out.println("\nBuffer get()");

        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        byte b = buffer.get();
        System.out.println("Return value = 0x" + Integer.toHexString((int)b & 0xff));
        ByteBufferUtility.printByteBuffer(buffer);

Buffer get()
P                                                           LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

Return value = 0x60
    P                                                       LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

get(byte[] dst) メソッドは dst のサイズだけ読み込みを行います。position もサイズ分移動します。

Buffer get(byte[] dst)

 

        System.out.println("\nBuffer get(byte[] dst)");

        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();
 
        byte[] dst = new byte[5];
        buffer.get(dst);
        System.out.println("dst is " + bytesToString(dst));
        ByteBufferUtility.printByteBuffer(buffer);

Buffer get(byte[] dst)
        P                                                   LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

dst is [20, bb, 38, 51, d9]
                            P                               LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

次の get(byte[] dst, int offset, int length) メソッドはちょっと癖があります。この動作は次に示すコードと同様の結果になります。

        for (int i = offset ; i < offset + length ; i++){
            dst[i] = buffer.get()
        }

length バイト読み込むのですが、それを dst の始めから格納するのではなく offset 分だけずれて入れていくようになります。

Buffer get(byte[] dst, int offset, int length)

 

        System.out.println("\nBuffer get(byte[] dst, int offset, int length)");

        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        dst = new byte[5];
        buffer.get(dst, 1, 3);
        System.out.println("dst is " + bytesToString(dst) + ", offset = 1, length = 3");
        ByteBufferUtility.printByteBuffer(buffer);

Buffer get(byte[] dst, int offset, int length)
                            P                               LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

dst is [0, d4, 7a, cb, 0], offset = 1, length = 3
                                        P                   LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

最後に残ったのが絶対アクセスを行う get(int index) メソッドです。このメソッドでは position は移動しません。

Buffer get(int index)

 

        System.out.println("\nBuffer get(int index)");

        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        b = buffer.get(2);
        System.out.println("index = 2, return value = 0x" 
	                           + Integer.toHexString((int)b & 0xff));
        ByteBufferUtility.printByteBuffer(buffer);

Buffer get(int index)
                                        P                   LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

index = 2, return value = 0x20
                                        P                   LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

ここまでは普通に get メソッドを使用してきましたが、position = limit の時に get メソッドを使うとどうなるでしょうか。2 つの場合をやってみました。

  • position = limit の時に get() をコールする
  • limit - position < dst.length になる時に get(byte[] dst) をコールする
        System.out.println("\nBuffer get() position = limit の場合");
        buffer.limit(10);
        buffer.position(buffer.limit());
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        try{
            b = buffer.get();
            System.out.println("Return value = 0x" + Integer.toHexString((int)b & 0xff));
        }catch(BufferUnderflowException ex){
            ex.printStackTrace();
        }
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println(
               "\nBuffer get(byte[] dst) dst のサイズが残りのバイト数より大きかったら");
        buffer.limit(buffer.capacity());
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        dst = new byte[20];
        try{
            buffer.get(dst);
            System.out.println("Values are " + bytesToString(dst));
        }catch(BufferUnderflowException ex){
            ex.printStackTrace();
        }
        ByteBufferUtility.printByteBuffer(buffer);

これを実行してみると 次のように BufferUnderflowExcepton が発生しました。例外が発生するのはあたりまえですね ^^;; ただし、このときに position などの変化はありませんでした。

Buffer get() position = limit の場合
                                        PL                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

java.nio.BufferUnderflowException
        at java.nio.Buffer.nextGetIndex(Buffer.java:367)
        at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:106)
        at BufferTest3.<init>(BufferTest3.java:65)
        at BufferTest3.main(BufferTest3.java:101)
                                        PL                   C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

Buffer get(byte[] dst) dst のサイズが残りのバイト数より大きかったら
                                        P                   LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

java.nio.BufferUnderflowException
        at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:116)
        at java.nio.ByteBuffer.get(ByteBuffer.java:580)
        at BufferTest3.<init>(BufferTest3.java:79)
        at BufferTest3.main(BufferTest3.java:101)
                                        P                   LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

 

 
 

要素へのアクセス 書き込み編

 
 

Buffer オブジェクトへの書き込みには put メソッドを使用します。これにも相対/絶対アクセスの両方があり、引数によって決まります。やはり ByteBuffer クラスのメソッド一覧を示しておきます。

メソッド名 相対/絶対 説明
ByteBuffer put(byte b) 相対 position 位置に 1 要素書き込み
ByteBuffer put(byte[] src) 相対 position 位置から dst のサイズ分書き込み
ByteBuffer put(byte[] src, int offset, int length)
相対 position の位置から src[offset] からの要素を length バイト書き込み
ByteBuffer put(ByteBuffer src) 相対 position 位置から src のサイズ分書き込み
ByteBuffer put(int index, byte b) 絶対 index の位置の要素の書き込み

get メソッドと対応させれば大体使い方はお分かりになると思います。

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

put(byte b) メソッドは 1 つの要素を position の位置に書き込みます。メソッドをコールすると position が 1 つ進みます。

Buffer put(byte b)

 

        System.out.println("\nBuffer put(0x10)");
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        b = (byte)0x10;
        buffer.put(b);
        ByteBufferUtility.printByteBuffer(buffer);

Buffer put(0x10)
P                                                           LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

    P                                                       LC
 [10, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

put(byte[] src) メソッドは src を書き込みます。書き込む要素は src のサイズと同じです。position も書き込みを行った分だけ移動します。

Buffer put(byte[] src)

 

        System.out.println("\nBuffer put(byte[] src)");
        byte[] src = new byte[]{(byte)0x20, (byte)0x21,
                                (byte)0x22, (byte)0x23, (byte)0x24};
        System.out.println("src = " + bytesToString(src));
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        buffer.put(src);
        ByteBufferUtility.printByteBuffer(buffer);

Buffer put(byte[] src)
src = [20, 21, 22, 23, 24]
        P                                                   LC
 [10, 11, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

                            P                               LC
 [10, 11, 20, 21, 22, 23, 24, d4, 7a, cb, 93, 3d, be, 70, 39]

offset と length を指定できる put(byte[] src, int offset, int length) メソッドも get メソッドと同様にちょっと癖があります。この動作は次に示すコードと同様の結果になります。

        for (int i = offset ; i < offset + length ; i++){
            buffer.put(src[i]);
        }

length バイト書き込みますが、dst[0] からではなく dst[offset] から書き込んでいきます。

Buffer put(byte[] src, int offset, int length)

 

        System.out.println("\nBuffer put(byte[] src, int offset, int length)");

        src = new byte[]{(byte)0x30, (byte)0x31, (byte)0x32, (byte)0x33, (byte)0x34};
        System.out.println("src = " + bytesToString(src) + ", offset = 2, length = 2");
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        buffer.put(src, 2, 2);
        ByteBufferUtility.printByteBuffer(buffer);

Buffer put(byte[] src, int offset, int length)
src = [30, 31, 32, 33, 34], offset = 2, length = 2
                            P                               LC
 [10, 11, 20, 21, 22, 23, 24, d4, 7a, cb, 93, 3d, be, 70, 39]

                                    P                       LC
 [10, 11, 20, 21, 22, 23, 24, 32, 33, cb, 93, 3d, be, 70, 39]

put(ByteBuffer src) メソッドは src を Buffer オブジェクトにコピーされます。このとき src の position から limit までがコピーされます。

Buffer put(ByteBuffer src)

 

        System.out.println("\nBuffer put(ByteBuffer src)");
        ByteBuffer srcBuffer = ByteBuffer.allocate(6);
        srcBuffer.put((byte)0x40).put((byte)0x41).put((byte)0x42);
        srcBuffer.put((byte)0x43).put((byte)0x44).put((byte)0x45);
	srcBuffer.position(1);
	srcBuffer.limit(4);

        System.out.println("src:");
        ByteBufferUtility.printByteBuffer(srcBuffer);
        System.out.println();

        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        buffer.put(srcBuffer);
        ByteBufferUtility.printByteBuffer(buffer);

このソースの中で srcBuffer に要素を書き込むときに put メソッドを連ねて行っています。これは put メソッドの戻り値が ByteBuffer であることを利用しているのですが、使うのはほどほどにしておかないと分かりにくいソースになってしまうので気をつけましょう。

さて、実行してみます。

Buffer put(ByteBuffer src)
src:
    P           L       C
 [40, 41, 42, 43, 44, 45]

                                    P                       LC
 [10, 11, 20, 21, 22, 23, 24, 32, 33, cb, 93, 3d, be, 70, 39]

                                                P           LC
 [10, 11, 20, 21, 22, 23, 24, 32, 33, 41, 42, 43, be, 70, 39]

最後は絶対アクセスの put(int index, byte b) メソッドです。このメソッドではやはり position は移動しません。

Buffer put(int index, byte b)

 

        System.out.println("\nBuffer put(int index, byte b)");
        b = (byte)0x50;
        System.out.println("index = 2, b = 0x" + Integer.toHexString((int)b & 0xff));
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        buffer.put(2, b);
        ByteBufferUtility.printByteBuffer(buffer);

Buffer put(int index, byte b)
index = 2, b = 0x50
                                                P           LC
 [10, 11, 20, 21, 22, 23, 24, 32, 33, 41, 42, 43, be, 70, 39]

                                                P           LC
 [10, 11, 50, 21, 22, 23, 24, 32, 33, 41, 42, 43, be, 70, 39]

get メソッドと同様にエラーになるような場合を確かめて見ましょう。

  • position = limit の時に put(byte b) をコールする
  • limit - position < src.length になるような src を用いて、put(byte[] src) をコールする
        System.out.println("\nBuffer put(0x50) position == limit の場合");

        buffer.limit(10);
        buffer.position(buffer.limit());
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        b = (byte)0x60;
        try{
            buffer.put(b);
        }catch(BufferOverflowException ex){
            ex.printStackTrace();
        }
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println(
                  "\nBuffer put(byte[] src) src のサイズが残りのバイト数より大きかったら");
	buffer.position(5);
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        src = new byte[20];
        try{
            buffer.put(src);
            ByteBufferUtility.printByteBuffer(buffer);
        }catch(BufferOverflowException ex){
            ex.printStackTrace();
        }

これを実行すると get メソッドとはことなり BufferOverflowExcepton が発生します。やはり、position などに変化はありませんでした。

Buffer put(0x50) position == limit の場合
                                        PL                   C
 [10, 11, 50, 21, 22, 23, 24, 32, 33, 41, 42, 43, be, 70, 39]

java.nio.BufferOverflowException
        at java.nio.Buffer.nextPutIndex(Buffer.java:388)
        at java.nio.HeapByteBuffer.put(HeapByteBuffer.java:134)
        at BufferTest4.<init>(BufferTest4.java:82)
        at BufferTest4.main(BufferTest4.java:116)
                                        PL                   C
 [10, 11, 50, 21, 22, 23, 24, 32, 33, 41, 42, 43, be, 70, 39]

Buffer put(byte[] src) src のサイズが残りのバイト数より大きかったら
                    P                   L                   C
 [10, 11, 50, 21, 22, 23, 24, 32, 33, 41, 42, 43, be, 70, 39]

java.nio.BufferOverflowException
        at java.nio.HeapByteBuffer.put(HeapByteBuffer.java:154)
        at java.nio.ByteBuffer.put(ByteBuffer.java:718)
        at BufferTest4.<init>(BufferTest4.java:95)
        at BufferTest4.main(BufferTest4.java:116)

 

 
 

byte から他の型へ

 
 

ByteBuffer クラスの読み書きを説明してきましたが、これは他の IntBuffer クラスでも同じように行うことができます。

でも、はじめから IntBuffer クラスを allocate していればいいのですが、ストリームなどからバイト列を読みこみ、それを int の値として使用するなどというときは困ってしまいます。ようするに、はじめにバイト列ありきのときにどうするかですね。

普通は byte から int に変換するのは、4 byte を読み込み、それを組み合わせて int を作ります。例えば、こんな感じです。

        byte b3 = (byte)0x00;
        byte b2 = (byte)0x01;
        byte b1 = (byte)0x00;
        byte b0 = (byte)0x01;
	
        int x = (int)((((b3 & 0xff) << 24) |
                       ((b2 & 0xff) << 16) |
                       ((b1 & 0xff) <<  8) |
                       ((b0 & 0xff) <<  0)));

これはこれでいいのですが、いちいちこんなことをやるのも面倒ですね。と思って ByteBuffer の JavaDoc を見ていたら、ありました型の変換を行うメソッドが。

さっそく使ってみましょう。ということで、サンプルはこちら。

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

まずは、ByteBuffer から int の値を読み書きしてみましょう。

        // int の読み込み
        System.out.println("\nBuffer getInt()");
        int x = buffer.getInt();
        System.out.println("return value = "
                           + x + " : 0x" + Integer.toHexString(x));
        ByteBufferUtility.printByteBuffer(buffer);

        // int の書き出し
        x = 10;
        System.out.println("\nBuffer putInt(int value) value = " 
                           + x + " : 0x" + Integer.toHexString(x));
        buffer.putInt(x);
        ByteBufferUtility.printByteBuffer(buffer);

int の読み込みは getInt メソッド、書き込みは putInt メソッドを使用します。両者とも position の位置から 4 byte を読み/書きします。getInt メソッド、putInt メソッドはここで使った以外に絶対アクセス用のものも用意されています。

また、int 以外の読み書きを行うメソッドも getXXX/putXXX (XXX の部分に型が入る) が用意されています。

さて実行してみると、

Buffer getInt()
return value = 1622417595 : 0x60b420bb
                P                                           LC
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

Buffer putInt(int value) value = 10 : 0xa
                                P                           LC
 [60, b4, 20, bb, 00, 00, 00, 0a, 7a, cb, 93, 3d, be, 70, 39]

電卓で計算してみると確かに 0x60b420bb は 1622417595 でした ^^;;

また、putInt も確かに 0x0a が書き込まれているのが確認できます。

要素を 1 つ 1 つ読み書きするのはこれでいいのですが、まとめて全部 int にしたいというときには Buffer オブジェクトごと変換するメソッドが ByteBuffer クラスに用意されています。

        // IntBuffer への変換
        System.out.println("\nIntBuffer へ変換");

        IntBuffer intBuffer = buffer.asIntBuffer();

        System.out.println(intBuffer);
        System.out.println("Last index value = "
               + Integer.toHexString(intBuffer.get(intBuffer.limit() - 1)));

asXXXBuffer メソッドは ByteBuffer を XXXBuffer (XXX には型の名前) に変換します。上の例だと asIntBuffer メソッドを使って IntBuffer オブジェクトに変換しています。このとき、position から limit の間の要素が変換対象となります。

IntBuffer へ変換
java.nio.ByteBufferAsIntBufferB[pos=0 lim=1 cap=1]
Last index value = 0x7acb933d

position = 8 だったので、IntBuffer にすると残りの [7a, cb, 93, be, 70, 39] が IntBuffer オブジェクトへの対象となります。これが int のバイト数 4 で割り切れればいいのですが、割り切れないときは 4 に満たない部分が切り捨てられてしまいます。

最終的に [7a, cb, 98, be] が残って、1 つの要素をもつ IntBuffer オブジェクトができます。値を見るとちゃんと 0x7acb933d になっていることが分かるとおもいます。

さて、ここまできて気になることを思いつきました。それは Endian です。

Big Endian と Little Endian では同じ int の値でも、バイト列としてみると異なります。今までの例を見ていると、どうやら Big Endian になっているようですが、Little Endian でも扱うことができるのでしょうか。

答えは可能です。

New I/O では ByteOrder クラスという Endian を表すためのクラスが導入されています。これを使用して、ByteBuffer オブジェクトの Endian を指定することができます。

ただし、デフォルトの Endian は先ほどの例で示したように Big Endian になっています。

        // Endian の変換
        System.out.print("\nEndian の変換: " + buffer.order());

        buffer.order(ByteOrder.LITTLE_ENDIAN);

        System.out.println(" -> " + buffer.order());
        buffer.position(0);
        ByteBufferUtility.printByteBuffer(buffer);

        System.out.println("\nBuffer getInt()");
        x = buffer.getInt();
        System.out.println("return value = " + x + " : 0x" + Integer.toHexString(x));
        ByteBufferUtility.printByteBuffer(buffer);

ByteBuffer クラスの order メソッドは、引数なしだと Endian を返し、ByteOrder オブジェクトを引数にすると Endian を変更します。ByteOrder クラスは 2 つの定数オブジェクトが用意されています。それぞれは ByteOrder.BIG_ENDIAN と ByteOrder.LITTLE_ENDIAN になります。

さて、実行してみましょう。

Endian の変換: BIG_ENDIAN -> LITTLE_ENDIAN
P                                                           LC
 [60, b4, 20, bb, 00, 00, 00, 0a, 7a, cb, 93, 3d, be, 70, 39]

Buffer getInt()
return value = -1155484576 : 0xbb20b460
                P                                           LC
 [60, b4, 20, bb, 00, 00, 00, 0a, 7a, cb, 93, 3d, be, 70, 39]

Little Endian にしても、ByteBuffer オブジェクト自体のデータの並びは変更されません。でも、getInt メソッドした値は Little Endian で読み込まれたことが確認できます。

 

 
 

Buffer オブジェクトの操作

 
 

まだ、説明していない残された機能があります。この章ではそれらの機能を説明していきます。ただし、ここで説明する機能は Buffer クラスでは定義されておらず、派生クラスで定義されています。

Buffer オブジェクトの操作には次のようなものがあります。

メソッド名 説明
ByteBuffer compact() position と limit の間の要素を 0 の位置から書き込む。position は compact メソッドを施す前の limit - position に移動する。limit は capacity と同じ値に変更される。
ByteBuffer duplicate() Buffer オブジェクトの複製を作成する。position などもコピーされる。
ByteBuffer slice() position と limit の間の要素だけを持つ Buffer オブジェクトを作成する。position = 0, limit = capacity となる。
ByteBuffer wrap(byte[] array) array の要素をもつ Buffer オブジェクトを生成する。position = 0, limit = capacity = array.length となる。
ByteBuffer wrap(byte[] array, int offset, int length)
array の要素を保持する Buffer オブジェクトを生成する。
ただし、position = offset, limit = offset + length, capacity = array.length となる。

説明だけだとよく分からないので、サンプルを使って確かめていきましょう。

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

Compact

はじめは compact メソッドからです。compact メソッドを実行すると position と limit の間にある [51, d9, d4, 7a, cb, 93, 3d] がBuffer オブジェクトの先頭にコピーされています。また、position の位置はコピーされた最後の要素の後になり、limit は capacity と同じになります。

        // Compacting
        System.out.println("\nBuffer compact()");
        buffer.position(5);
        buffer.limit(12);
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();
        ByteBufferUtility.printByteBuffer(buffer.compact());

さっそく、実行。

Buffer compact()
                    P                           L           C
 [60, b4, 20, bb, 38, 51, d9, d4, 7a, cb, 93, 3d, be, 70, 39]

                            P                               LC
 [51, d9, d4, 7a, cb, 93, 3d, d4, 7a, cb, 93, 3d, be, 70, 39]

comapct メソッドは直接 Buffer オブジェクトに行うので compact メソッドの戻り値の Buffer オブジェクトはもともとの Buffer オブジェクトと同一のものになります。

Duplicate

次は簡単です。duplicate メソッドは Buffer オブジェクトのコピーを作ります。

        // Duplicating
        System.out.println("\nBuffer duplicate()");
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();
 
        ByteBuffer dupBuffer = buffer.duplicate();
        ByteBufferUtility.printByteBuffer(dupBuffer);

実行してみると、コピーが作られているのが確認できます。

Buffer duplicate()
                            P                               LC
 [51, d9, d4, 7a, cb, 93, 3d, d4, 7a, cb, 93, 3d, be, 70, 39]

                            P                               LC
 [51, d9, d4, 7a, cb, 93, 3d, d4, 7a, cb, 93, 3d, be, 70, 39]

ただし、コピーといっても、Buffer オブジェクトが保持している要素自体のコピーは行いません。position, limit, capacity はコピーされて、元の Buffer オブジェクトのそれとは別になります。

例えば、duplicate メソッドを使用してコピーした Buffer オブジェクトに書き込みを行ってみました。

	// コピーした Buffer を操作する
	System.out.println("\nコピーした Buffer を操作");
	dupBuffer.position(0);
	dupBuffer.put((byte)0x10);

        System.out.println("Original Buffer");
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();

        System.out.println("Duplicated Buffer");
        ByteBufferUtility.printByteBuffer(dupBuffer);

コピーを行った後に、position を 0 にし、0x10 を書き込んでいます。これを実行してみると、

コピーした Buffer を操作
Original Buffer
                            P                               LC
 [10, d9, d4, 7a, cb, 93, 3d, d4, 7a, cb, 93, 3d, be, 70, 39]

Duplicated Buffer
    P                                                       LC
 [10, d9, d4, 7a, cb, 93, 3d, d4, 7a, cb, 93, 3d, be, 70, 39]

元の Buffer オブジェクトも、コピーした Buffer オブジェクトも要素が変化しているのがお分かりだと思います。ただし、position の値は両者で異なっています。

このようなコピーのことを shallow copy といいます。これに対して、要素まですべてコピーするようなコピーを deep copy といいます。

Java ではオブジェクトのコピーを行うときなどは一般的に shallow copy になります。例えば、Object クラスの clone メソッドは shallow copy を行います。

Slice

slice メソッドは Buffer オブジェクトの一部を切り出して、新たに Buffer オブジェクトを生成します。

        // Slicing
        System.out.println("\nBuffer slice()");
        buffer.limit(12);
        ByteBufferUtility.printByteBuffer(buffer);
        System.out.println();
        ByteBufferUtility.printByteBuffer(buffer.slice());

position と limit の間の要素 [d4, 7a, cb, 93, 3d] だけを切り出して、新たに Buffer オブジェクトを作ります。

Buffer slice()
                            P                   L           C
 [51, d9, d4, 7a, cb, 93, 3d, d4, 7a, cb, 93, 3d, be, 70, 39]

P                   LC
 [d4, 7a, cb, 93, 3d]

slice メソッドを使用しても、もとの Buffer オブジェクトは変更されません。slice メソッドの戻り値の Buffer オブジェクトとは異なるオブジェクトになります。

Wrap

wrap メソッドは byte 配列から ByteBuffer オブジェクトを生成するためのメソッドで static メソッドになります。他のクラスも対応するプリミティブの配列からそのクラスのオブジェクトを生成する wrap メソッドを持ちます。

        // Wrapping
        System.out.println("\nCreate ByteBuffer using wrap()");
        byte[] array = new byte[]{(byte)0x20, (byte)0x21,
                                  (byte)0x22, (byte)0x23,
                                  (byte)0x24};
        System.out.println("array = " + bytesToString(array));
        System.out.println();
        ByteBufferUtility.printByteBuffer(ByteBuffer.wrap(array));

wrap メソッドには offset と length を指定できるものもありますが、ここでは単純に byte 配列を引数にするものを使ってみました。

Create ByteBuffer using wrap()
array[] = [20, 21, 22, 23, 24]

P                   LC
 [20, 21, 22, 23, 24]

allocate メソッドは初期値を渡せないのですが、wrap メソッドを使用すれば初期値のある Buffer オブジェクトを生成することができます。

 

 

 

 

ヒープの外に

 
 

まだ、説明していない機能にメモリにマップされたファイルへのアクセスと Java のヒープ外のメモリへの直接アクセスがあります。しかし、メモリマップファイルは Channel と一緒に説明したほうがいいので今回はやりません。

したがって、最後はヒープ外のメモリのアクセスです。

ヒープ外のメモリをアクセスするといっても、結局は Buffer オブジェクトの要素をヒープの外に作るということです。その Buffer オブジェクを使用して、要素にアクセスすればヒープ外のメモリにアクセスすることになります。

ヒープの外に作るというのは C や C++ の配列と同じになります。Java の配列は、配列といっても実際には配列クラスのオブジェクトになっています。

この配列のクラスは終端の処理などをやってくれたりするので、使う側にとってはいろいろと利点があります。しかし、この処理をしているために、C/C++ の配列に比較すると処理速度が若干落ちてしまうという欠点があります。

そこで、Buffer クラスではヒープの外に C や C++ で作った配列をおき、それをアクセスすることができます。このような Buffer オブジェクトは通常の allocate メソッドを使用して生成するのではなく、allocateDirect メソッドを使用して生成します。

ただし、allocateDirect メソッドは ByteBuffer クラスにしか定義されていません。

allocateDirect メソッドで生成してしまえば、使い方は通常の Buffer オブジェクトと同じです。とはいっても、allocateDirect メソッドで生成した Buffer オブジェクトが GC されなければ、ヒープ外にメモリをアロケートしたままになってしまい、下手をするとメモリリークを引き起こしたりします。ですから、使うときには細心の注意を払ってください。

さて、allocateDirect で生成した Buffer オブジェクトと普通の Buffer オブジェクトの違いはなんといってもパフォーマンスです。簡単なプログラムでどれくらいパフォーマンスが違うか試してみましょう。

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

BufferTest7 クラスでは capacity の同じ Buffer オブジェクトを 2 つ生成しています。一方が allocate メソッド、もう一方が allocateDirect メソッドを使用しています。

        ByteBuffer normalBuffer = ByteBuffer.allocate(      100000000);
        ByteBuffer directBuffer = ByteBuffer.allocateDirect(100000000);

この 2 つのオブジェクトに対して、先頭からすべての要素を get するという比較をしてみました。比較する部分は次のようにしています。

    private void measure(ByteBuffer buffer){
 
        long start = System.currentTimeMillis();
        while(buffer.hasRemaining()){
            buffer.get();
        }
        long end = System.currentTimeMillis();
 
        System.out.println("Total time = " + (end - start));
    }

さっそく実行してみたいのですが、Buffer オブジェクトの capacity があまりにも大きいので普通に実行すると OutOfMemoryError が発生してしまいます。そこで、-Xmx オプションを使用して最大メモリを指定することにしました。-Xmx のデフォルト値は 64MB なので、それ以上の 256MB などで実行してみてください。

C:\>java -Xmx256M BufferTest7
Normal Buffer: java.nio.HeapByteBuffer[pos=0 lim=100000000 cap=100000000]
Direct Buffer: java.nio.DirectByteBuffer[pos=0 lim=100000000 cap=100000000]

普通の Buffer オブジェクト
Total time = 4907

Direct Buffer オブジェクト
Total time = 3685

筆者の環境は CPU が Athlon 800MHz, Memory 512MB で、Windows 2000 上で実行させてみました。単なる get メソッドを用いた読み込みだけでも 1 秒以上の差があります。有効に使えば、パフォーマンスがかなり向上するのではないでしょうか。

実行すると、 2 種類の Buffer オブジェクトの情報を出力しています。この部分の実装は次のようになっています。単にオブジェクトを println メソッドを使用して書き出しているだけです。

        ByteBufferUtility.initByteBuffer(normalBuffer);
        System.out.println("Normal Buffer: " + normalBuffer);
 
        ByteBufferUtility.initByteBuffer(directBuffer);
        System.out.println("Direct Buffer: " + directBuffer);

この出力結果を見ると、allocate メソッドで生成されたバッファは HeapByteBuffer クラス、allocateDirect メソッドでは DirectByteBuffer クラスになっています。図 1 に示したようにどちらも ByteBuffer クラスの派生クラスになっています (DirectByteBuffer クラスは ByteBuffer クラスの孫クラスですが)。このため、両者とも ByteBuffer オブジェクトとして使うことはできますが、要素のアロケートの実装方法によりクラスを分けているということが分かります。

この 2 つのクラスは public なクラスではないので、JavaDoc には記載されていません。というのも、ユーザにとっては使い方が同じであれば、実装方法は気にしないでいいからだと思います。オブジェクト指向のカプセル化の例の 1 つになっているのではないでしょうか。

ところで、先ほど allocateDirect メソッドは ByteBuffer クラスにしかないと説明しましたが、IntBuffer クラスでも Direct な Buffer オブジェクトを使いたいと思うことはあると思います。さて、どうしましょう。

答えは簡単で allocateDirect メソッドで生成した ByteBuffer オブジェクトを asIntBuffer メソッドを使用して IntBuffer オブジェクトに変換すればいいのです。この方法で他の Buffer クラスの派生クラスでも Direct なオブジェクトを使用することができます。

 

 
 

最後に

 
 

配列とも Collection とも違う Buffer クラスでが、なかなか使いでがありそうです。プリミティブに関しては今まで Collection を使いたくても使えないので、配列しか選択の余地はなかったのですが、Buffer クラスが加わったことで選択の範囲が広がりました。

Buffer クラスを使うと配列にはないいろいろな特徴があるので、これらを有効に使っていきたいですね。

特に Channel などの他の New I/O で導入されたクラスと一緒に使うと効果が大きいようです。JavaOne 2001 ではグランドキャニオンの CG のデモが公開されました。これは以下の Web Page で公開されています。

Grand Canyon Demo Page
http://java.sun.com/products/jfc/tsc/articles/jcanyon/

このデモでは約 100MB のデータを Channel と Buffer を使用して読み込んでいるようです。実際に実行して New I/O を使ったときと使わないときの差を肌で感じてみてください。

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

参考 URL

(Sep. 2001)

 

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