チャネル技術には、バッファが欠かせません。 このページでは、バッファの基礎と、基本的な使い方について記述します。
一般的な通称としてのバッファは、データを溜めておくためのプールのことです。 配列であっても(C言語では)ポインタであっても、ある一定量のデータを溜めておくことができる領域のことを言います。 java.nioには、この通称としてのバッファではなくて、クラスとしてのBufferが提供されています。 チャネルで使用するバッファとは、この、クラスとしてのBufferです。
java.nioで提供されているバッファは、可能であれば、 JavaVM上のメモリを使用せずに、 システム・リソース上のメモリを直接使用 します。 これにより、より高速でデータの格納と取り出しを行うことができるように設計されています。
java.nioには、各プリミティブ型ごとに、クラスが提供されています (ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, MappedByteBuffer, ShortBuffer)。 そのうち、基礎となっているのが、 ByteBuffer です。 まずは、ByteBufferを中心に説明をしていきます。
バッファは、ByteBuffer#allocate(int)とByteBuffer#allocateDirect(int)により割り当てることができます。 allocateメソッドは、JavaVM上にバッファを割り当て、allocateDirectメソッドは、可能であれば、JavaVMの外側の、 システムリソース上に直接割り当てます(可能でなければallocateメソッドと同じ動作になる)。
バッファには、次の3つの必須プロパティーが存在します。
v位置(position) vリミット(limit) v容量(capacity)
+---+---+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+---+---+---+---+---+---+---+---+---+---+
これらは、概念的には、要素の中間に位置します。 上記例だと、位置は1、リミットは5、容量は10になります。(こう考えるほうが考えやすい) なお、容量、リミット、位置には、次の関係が成り立ちます。
0 <= 位置 <= リミット <= 容量
バッファの基本の理解と使い方の説明では、以下のメソッドを使用します。
// capacity要素分バイトバッファを(可能ならJavaVM外に)割り当てる Buffer allocateDirect(int capacity) // 位置を取得する Buffer Buffer#position() // 位置を設定する Buffer Buffer#position(int newPosition) // リミットを取得する Buffer Buffer#limit() // リミットを設定する Buffer Buffer#limit(int newLimit) // 容量を取得する(設定はできない) Buffer Buffer#capacity() // 現在のpositionにバイトを書き込み、positionをインクリメントする ByteBuffer ByteBuffer#put(byte b) // 現在のpositionからバイトを読み取り、positionをインクリメントする byte ByteBuffer#get()
バッファを作成して、そこにデータを格納して、格納したデータをすべて読み取るサンプルプログラムをつくってみます。
import java.nio.ByteBuffer;
public class FirstByteBuffer {
public static void main(String[] args) {
// 9文字(9バイト)のデータ
byte[] originalData = "test data".getBytes();
// 要素数10のバッファ
ByteBuffer buff = ByteBuffer.allocateDirect(10);
// ここからデータの格納処理 //
// バッファにデータを格納する
for (int i=0 ; i < originalData.length ; i++) {
buff.put(originalData[i]);
}
// バッファからのデータの取得に備え、
// リミットを現在のポジションにセット
buff.limit(buff.position());
// バッファからのデータの取得に備えてポジションを0にセット
buff.position(0);
// バッファからのデータの取得処理 //
// buffに入っているデータの量だけバイト配列を割り当てる
byte[] obtainedData = new byte[buff.limit()];
// 0番目の要素から順にbuffのデータを取得
for (int i=0 ; i < buff.limit(); i++) {
obtainedData[i] = buff.get();
}
// バイト配列を文字列に変換して表示
System.out.println(new String(obtainedData));
}
}
test data
要素数10のバッファを作成した直後のバッファの状態です。
v位置 v容量とリミット +---+---+---+---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +---+---+---+---+---+---+---+---+---+---+
バッファの作成直後は、位置が0、リミットは容量と同じ場所にあります。
バッファにデータを格納し終わった後のバッファの状態です。
位置 容量とリミット
v v
+---+---+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+---+---+---+---+---+---+---+---+---+---+
putメソッドが9回呼ばれたので、位置も0から9まで移動しています。
データの取得に備えて位置とリミットを操作し終わった後のバッファの状態です。
位置 リミット 容量 v v v +---+---+---+---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +---+---+---+---+---+---+---+---+---+---+
位置が0になり、次のgetでは0番目の要素が読み込めるようになっています。 そしてリミットは9になり、格納されているデータの量を示しています。
バッファからデータを取得した後のバッファの状態です。
位置
リミット 容量
v v
+---+---+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+---+---+---+---+---+---+---+---+---+---+
位置がリミットになるまで読まれ、バッファのデータがすべて読み終わったことを示しています。
このように、バッファの操作は、位置、リミット、容量の3つを駆使して行われます。
バッファの操作を、プログラム開発の中で毎回行っていたのでは、効率がよくありません。 それらの操作を、もう少し論理的、抽象的に行うために用意されたメソッドが、 clear、flip、compactです。
clearメソッドの説明をします。 clearメソッドは、バッファを初期状態に戻します。 初期状態というのは、バッファが作成された状態です。 つまり、以下のような状態になります。
v位置 v容量とリミット +---+---+---+---+---+---+---+---+---+---+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +---+---+---+---+---+---+---+---+---+---+
flipメソッドの説明をします。 flipメソッドは、バッファにputしてあるデータを、getする準備をします。 これは、先ほど示したプログラムの、この部分と同じです。
// バッファからのデータの取得に備え、リミットを現在のポジションにセット buff.limit(buff.position()); // バッファからのデータの取得に備えてポジションを0にセット buff.position(0);
そして最後に、compactメソッドです。 compactメソッドは、すでにgetしたデータを切り捨てて、 位置からリミットまでのスペースが最大になるように確保します。
位置 リミット 容量
v v v
+---+---+---+---+---+---+---+---+---+---+
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+---+---+---+---+---+---+---+---+---+---+
内容 | a | b | c | d | e | f | g | h | | |
+---+---+---+---+---+---+---+---+---+---+
a,b,c,dのデータまでgetした後に、putをしたくなったとき、compactを実行すると、以下のようになる。
リミット
位置 容量
v v
+---+---+---+---+---+---+---+---+---+---+
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+---+---+---+---+---+---+---+---+---+---+
内容 | e | f | g | h | | | | | | |
+---+---+---+---+---+---+---+---+---+---+
つまり、すでにgetした内容はバッファから捨てているわけです。 そして、次回以降のput操作は位置4から行われます。
putメソッドとgetメソッドは、一回の呼び出しにつき、1バイトしか操作ができませんでした。 しかし、数Mバイトある場合にそのバイト数の回数だけputメソッドやgetメソッドを呼び出していたのでは、 オーバーヘッドでかなりの処理時間をロスしてしまいます。 そのようなことがないように、なるべくならば、変わりに以下のメソッドを使用すべきです。
ByteBuffer get(byte[] det) // dstにdst.length分だけデータをgetする ByteBuffer put(byte[] src) // scrからsrc.length分だけデータをputする
今回は、データをバッファに書き込んで、そのうちの一部のデータを取得し、さらに追加でバッファに書き込み、 最後に、バッファからすべてのデータを取得するという、少し複雑な操作をしてみます。
import java.nio.ByteBuffer;
public class MoreLocalBufferManipulate {
public static void main(String[] args) {
byte[] firstData = "first data".getBytes();
byte[] secondData = "second data".getBytes();
ByteBuffer buff = ByteBuffer.allocateDirect(16);
// firstDataをバッファに書き込み
buff.put(firstData);
// バッファから5バイト分getして表示する
buff.flip();
byte[] obtainedByte1 = new byte[5];
buff.get(obtainedByte1);
System.out.println(new String(obtainedByte1));
// バッファにsecondDataを書き込む
buff.compact();
buff.put(secondData);
// バッファからすべてのデータをgetして表示する
buff.flip();
byte[] obtainedByte2 = new byte[buff.limit()];
buff.get(obtainedByte2);
System.out.println(new String(obtainedByte2));
}
}
first datasecond data