S-JIS[2009-01-16] 変更履歴

Buffer(ByteBuffer・CharBuffer)

JDK1.4で新設されたnew I/O関連に、バッファークラスがある。
でもちょっと癖があって分かりづらい(苦笑)


概要(特徴)

Bufferの用途・存在意義は、「バッファー」という名前が示す通り、データを保存(追加)し、そこから取得する為のもの。ではあるのだけれど。

Bufferを使う上でややこしいのが、そのBufferインスタンスを今「書き込みに使用しているのか」「読み取りに使用しているのか」をプログラマーが把握・意識して、それに応じた使い方(メソッド呼び出し)をしなければならない(制御する必要がある)こと。
また、同一メソッドでも、読み取りの場合と書き込みの場合で意味合いが異なるものがある。→メソッド一覧

java.nio.Bufferは全てのバッファーの親である抽象クラスであり、データ型に応じて実装クラスがある。
例えばbyteを扱うのはByteBuffercharを扱うのはCharBuffer
ただし、実際にはさらにByteBufferやCharBufferを継承したクラス(publicでない)が使われる。


データ例

Bufferは、position・limit・capacityというパラメーターを持っている。
capacityは、バッファーサイズ(上限)。不変。
positionは、バッファー内での読み書きをする開始/現在位置。
limitは、書き込みの場合、書き込める最大サイズ(書き込める上限)。読み取りの場合、保持しているデータサイズ(読み込める上限)。

capacityは変わらないが、positionやlimitは読み書き操作に応じて変わっていく。
書き込みも読み取りも、現在のpositionの位置から開始する。

  処理内容 処理後のバッファー内の様子 解説
1 //バッファーインスタンスを生成する
ByteBuffer bb = ByteBuffer.allocate(16);
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   …データ
↑position ↑capacity
↑limit
allocate()
バッファーを作成する。
positionは0、
limitはcapacityになる。
2 //バッファーへ書き込む
bb.put((byte)0xaa);
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
aa 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   …データ
  ↑position ↑capacity
↑limit
put()
positionの位置に書かれ、
書き込んだ分だけ
positionが動く。
もしlimitまで到達して
なお書き込もうとしたら、
オーバーフロー(例外発生)。
3 //バッファーへ書き込む
bb.put((byte)0xbb);
bb.put((byte)0xcc);
bb.put((byte)0xdd);
bb.put((byte)0xee);
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
aa bb cc dd ee 00 00 00 00 00 00 00 00 00 00 00   …データ
  ↑position ↑capacity
↑limit
4 //読み込み状態に変更する
bb.flip();
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
aa bb cc dd ee 00 00 00 00 00 00 00 00 00 00 00   …データ
↑position ↑limit ↑capacity
flip()は、
limitをpositionの値に設定し
positionを0にする。
5 //バッファーから読み取る
byte data = bb.get();
//dataの値は0xaa
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
aa bb cc dd ee 00 00 00 00 00 00 00 00 00 00 00   …データ
  ↑position ↑limit ↑capacity
get()
positionの位置から読まれ、
読み込んだ分だけ
positionが動く。
6 //バッファーから読み取る
while (bb.hasRemaining()) {
 data = bb.get();
}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
aa bb cc dd ee 00 00 00 00 00 00 00 00 00 00 00   …データ
  ↑limit
↑position
↑capacity
読取時のhasRemaining()は、
残りデータが有ればtrue。
positoin=limitになるとfalse。
7 //バッファーをクリアする
bb.clear();
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
aa bb cc dd ee 00 00 00 00 00 00 00 00 00 00 00   …データ
↑position ↑capacity
↑limit
clear()は、
positionを0、
limitをcapacityに戻す。
(データ自体は消されない)
8 2(書き込み開始)へ戻る
6' 上記5の直後(データが残っている状態)で
//追加書き込みできる状態にする
bb.compact();
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
bb cc dd ee ee 00 00 00 00 00 00 00 00 00 00 00   …データ
  ↑position ↑capacity
↑limit
compact()
読まれなかったデータを
0の位置に移動させ、
positionを続きの位置に移す。
limitはcapacityに戻す。
7' //バッファーへ書き込む
bb.put((byte)0x11);
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 …位置
bb cc dd ee 11 00 00 00 00 00 00 00 00 00 00 00   …データ
  ↑position ↑capacity
↑limit
(データが追加される)

注意点

バッファー作成直後にいきなり読み取ろうと(get)すると、limitはcapacity位置なので、いきなり最大サイズまで読み込むことになってしまう。

また、書き込み(put)し終わった後flip()せずに読み込もうとすると、positionは書き込んだ直後の位置なのでそこから読み取りをスタートすることになり、書き込んだデータでなく初期化されている(あるいは以前書き込んだ)データをlimit位置(=capacity)まで読み込むことになってしまう。

同様に、全部読み終わった後clear()せずに書き込もうとすると、positionがlimit位置に居るのでそれ以上書き込めない。 いきなりオーバーフローになってしまう。

読んだ後clear()でなくflip()したとすると、(limitにはpositionが代入されるので)limitは最後に読み込んだ直後の位置となり、positionは0になる。
この状態で書き込み(put)を始めると、前回読み込んだサイズ(=limit位置)までしか書き込めないという妙な事態になる。


Bufferのメソッド

Bufferには、以下のようなメソッドがある。
(中には、バッファーの実装特有(ByteBuffer・CharBuffer等の専用)のメソッドもある)

※「クラス」のBはBuffer、BBはByteBuffer、CBはCharBuffer
※「使用可能タイミング」とは、そのメソッドを呼び出すことに意味のある状態
※「実行後のモード」は、メソッド呼出し後に可能な、意味のある操作ができる状態

メソッド クラス 使用可能
タイミング
説明(処理の内容) 実行後のバッファーのモード
B BB CB
allocate(サイズ)   初期化 バッファーインスタンスを生成する。 書き込める状態になっている
allocateDirect(サイズ)     ByteBufferでは、Javaのヒープ外(マシンネイティブ)に領域を確保できる。
一般的に最初の領域確保と最後の解放にはコストがかかるが、読み書き速度は高速。
wrap(配列)   指定された配列内に読み書きする為のバッファーを生成する。
配列 = array() 常に (JDK1.6以降)
バッファー内部でJavaの配列を保持している場合、その配列を返す。
その配列のデータを書き換えるとバッファー内にも影響がある。
共変戻り値型を使用しているので、Buffer#array()の戻り型はObjectだが、ByteBufferはbyte[]、CharBufferはchar[]を返す)
変更なし
put(データ)
put(配列)
  書込状態 position位置にデータを書き、書き込んだ分だけpositionを加算する。
limitを超えたらオーバーフロー。
変更なし
(書き込める状態のまま)
append(データ)     JDK1.5で追加されたメソッド。
だが、中身はput()なので素直にput()を使った方がいいだろう。
flip() limitを現在のpositionに設定し、positionを0にする。
すなわち、今まで書き込んだデータを先頭から読めるようにする。
読み込める状態になる
変数 = get()
get(配列)
  読込状態 position位置のデータを読み、読み込んだ分だけpositionを加算する。
limitを超えたらオーバーフロー。
変更なし
(読み込める状態のまま)
変数 = toString() CharBufferでは、char配列をStringにして返す。
この場合、positionやlimitは変更されない。
rewind() positionを0にする。limitは変わらない。
すなわち、データを先頭から再度読み込めるようになる。
clear() 両方 positionを0にし、limitをcapacityに戻す。
(内部で保持しているデータが00に初期化されるわけではない)
書き込める状態になる
compact()   読込状態 読み取られずに残っているデータをバッファーの先頭に移動させて(詰めて)、positionをその直後にし、limitをcapacityに戻す。
すなわち、追加書き込みが出来るようになる。
remaining() 書込状態 limit - positionを返す。
すなわち、書き込める残りのサイズを返す。
変更なし
読込状態 limit - positionを返す。
すなわち、読み取れる(残っている)データのサイズを返す。
hasRemaining() 書込状態 remaining()>0のときtrue。
すなわち、書き込みできる余地が残っているとき、真。
読込状態 remaining()>0のときtrue。
すなわち、読み込めるデータがまだ残っているとき、真。
mark() 両方 現在のpositionを記憶(mark)する。reset()でその位置へ戻れる。 変更なし
reset() 記憶されていた位置(mark)へpositionを移動する。
mark()後にflip()clear()等のメソッドを呼ぶとmarkもクリアされるので、その場合は移動できない(例外が発生する)。
位置 = position() 常に 現在のpositionを返す。 設定系の場合、値による
取得系の場合、変更なし
position(位置) positionをセットする。0≦位置≦limitでないとエラー(例外発生)。
位置 =  limit() 現在のlimitを返す。
limit(位置) limitをセットする。0≦位置≦capacityでないとエラー(例外発生)。
positionがlimitより大きくなったら、positionはlimit位置に変更される。
サイズ = capacity() capacityを返す。

1文字ずつ文字コードを変換する例

文字コードを変換するCharsetDecoder#decode()CharsetEncoder#encode()は、入出力にバッファーを使用する。
(ファイルの文字コードを変換しつつ読み込むにはInputStreamReaderを使えばよいが、ここでは敢えて1文字ずつ変換するメソッドを自作してみる)

	public static void main(String[] args) throws IOException {

		String encoding = "MS932";
		Charset cs = Charset.forName(encoding);

		FileInputStream fis = new FileInputStream("ファイル名");
		try {
			decode(fis, cs);
		} finally {
			fis.close();
		}
	}
	public static void decode(InputStream is, Charset cs) throws IOException {
		CharsetDecoder decoder = cs.newDecoder();
		boolean end = false; //ファイルの終わりまで読み込んだかどうか

		// バッファーを用意する。
		// 今回は1文字ずつしか変換しないので、そんなに大きなバッファーは要らない
		ByteBuffer bb = ByteBuffer.allocateDirect(16); // bbは書き込める状態
		CharBuffer cb = CharBuffer.allocate(16);        // cbは書き込める状態

		do {
			int d = is.read();
			if (d >= 0) {
				System.out.printf("%02x→", d);
				bb.put((byte) d); // bbへの書き込み
			} else {
				end = true;
			}

			bb.flip(); // bbを読み込み状態に変更

			CoderResult cr = decoder.decode(bb, cb, end); // bbから読み込み、cbへ書き込む
			if (!cr.isUnderflow()) {
				cr.throwException();
			}

			if (end) {
				cr = decoder.flush(cb); // cbへ書き込む
				if (!cr.isUnderflow()) {
					cr.throwException();
				}
			}

			cb.flip(); // cbを読み込み状態に変更

			while (cb.hasRemaining()) { // cbに読み込めるデータがある場合
				char c = cb.get(); // cbから読み込み
				System.out.printf("[%c]%n", c);
			}

			if (bb.hasRemaining()) {	// bbに読み込めるデータがまだ残っている場合
				bb.compact(); // データを詰めて書き込める状態に変更
			} else {
				bb.clear(); // 書き込める状態に変更
			}

			cb.clear(); // cbを書き込み状態に変更
		} while (!end);
	}

実行例:

30→[0]
31→[1]
32→[2]
33→[3]
41→[A]
42→[B]
43→[C]
61→[a]
62→[b]
63→[c]
82→a0→[あ]
82→a2→[い]
82→a4→[う]
8e→c0→[実]
8c→b1→[験]

CharsetDecoder#decode()は、ByteBufferのデータを(解釈できる限り)読み込んで、解釈できた文字だけをCharBufferに書き込む。
今回は1バイトずつ渡しているので、2バイト(以上の)文字は1バイト目では解釈できない。
その場合はCharBufferには何も書かれず、ByteBufferにも解釈不能データが残る(読込位置のpositionがlimitまで行かない)。そこでcompact()を呼び出して追加書き込みできるようにしている。
bb.hasRemaining()を使って条件判断せずに 常にcompact()を呼び出しても状態は同じになるが、compact()は内部で配列コピーを呼び出すので、clear()の方が軽い(高速)。


ファイルチャネルからバッファーに読み込む例

JDK1.4以降のFileInputStream(やFileOutputStream)からはファイルチャネルを取得することが出来る。
そして、 ファイルチャネルから読み出してByteBufferへ直接出力することが出来る。

	public static void main(String[] args) throws IOException {

		String encoding = "MS932";
		Charset cs = Charset.forName(encoding);

		FileInputStream fis = new FileInputStream("ファイル名");
		FileChannel fch = fis.getChannel();
		try {
			decodeCh(fch, cs);
		} finally {
			fis.close(); // 取得したチャネルもクローズされる
		}
	}
	public static void decodeCh(FileChannel ch, Charset cs) throws IOException {
		CharsetDecoder decoder = cs.newDecoder();
		boolean end = false; //ファイルの終わりまで読み込んだかどうか

		// バッファーを用意する。
		ByteBuffer bb = ByteBuffer.allocateDirect(256); // bbは書き込める状態
		CharBuffer cb = CharBuffer.allocate(256);        // cbは書き込める状態

		do {
			int len = ch.read(bb); // bbへ書き込み
			if (len < 0) {
				end = true;
			}

			bb.flip(); // bbを読み込み状態に変更

			CoderResult cr = decoder.decode(bb, cb, end); // bbから読み込み、cbへ書き込む
			if (!cr.isUnderflow()) {
				cr.throwException();
			}

			if (end) {
				cr = decoder.flush(cb); // cbへ書き込む
				if (!cr.isUnderflow()) {
					cr.throwException();
				}
			}

			cb.flip(); // cbを読み込み状態に変更
			String str = cb.toString();
			System.out.println(str);

			bb.compact(); // データを詰めて書き込める状態に変更
			cb.clear(); // cbを書き込み状態に変更
		} while (!end);
	}

なお、ファイルチャネルには、ファイル内を直接バッファーとして取得するメソッドも用意されている。
(ついでにCharsetDecoder#decode()にもCharBufferを生成して返すメソッドが用意されている)

	public static void decodeLump(FileChannel ch, Charset cs) throws IOException {
		CharsetDecoder decoder = cs.newDecoder();

		MappedByteBuffer bb = ch.map(MapMode.READ_ONLY, 0, ch.size());
		// bbは読み込める状態になっている

		CharBuffer cb = decoder.decode(bb); // bbから読み込んで、cbを返す
		// cbは読み込める状態になっている

		String str = cb.toString();
		System.out.println(str);
	}

Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま