ぐらめぬ・ぜぷつぇんのはてダ(2007 to 2011)

2007年~2011年ごろまで はてなダイアリー に書いてた記事を引っ越してきました。

java.nio.Buffer系の使い方が分からないので簡単なサンプルを作ってみた。

事情によりJDK1.5で確認。
元ネタはここ:Buffering « File Input Output « Java
まずは作ったサンプルの全体像。

package test.nio2;

import java.nio.CharBuffer;

public class BufferDemo2
{
	public static void dumpCharBuffer(CharBuffer b)
	{
		char c = '\0';
		while (b.hasRemaining()) {
			c = b.get();
			if (c != 0) {
				System.out.print("[" + c + "]: ");
			} else {
				System.out.print("[]: ");
			}
			BufferUtil.dump(b);
		}
	}
	
	public static void main(String[] args)
	{
		CharBuffer cb = CharBuffer.allocate(64);
		BufferUtil.dump(cb);
		cb.limit(16);
		BufferUtil.dump(cb);
		cb.put("UsingBuffers".toCharArray());
		BufferUtil.dump(cb);
		cb.rewind();
		BufferUtil.dump(cb);
		dumpCharBuffer(cb);
		System.out.println("-------------------------");
		try {
			cb.put("Foo".toCharArray());
			BufferUtil.dump(cb);
		} catch (java.nio.BufferOverflowException ignored) {
			System.out.println("Buffer Overflow!!");
		}
		System.out.println("-------------------------");
		cb.position(5);
		cb.mark();
		BufferUtil.dump(cb);
		cb.put("Foo".toCharArray());
		BufferUtil.dump(cb);
		cb.reset();
		BufferUtil.dump(cb);
		dumpCharBuffer(cb);
		System.out.println("-------------------------");
		cb.clear();
		BufferUtil.dump(cb);
		cb.put("BarBuz".toCharArray());
		cb.flip();
		BufferUtil.dump(cb);
		dumpCharBuffer(cb);
		System.out.println("-------------------------");
	}
}
BufferUtil.dump(cb);

というのが頻出しますが、要はjava.nio.Bufferのcapacity, limit, position, remainingを整形して標準出力する自作メソッドです。

capacity[64],limit[64],position[0],remaining[64]

こんな感じに整形して出力します。

細かい解説は後回しにして、まずは実行結果の全体を示します。

capacity[64],limit[64],position[0],remaining[64]
capacity[64],limit[16],position[0],remaining[16]
capacity[64],limit[16],position[12],remaining[4]
capacity[64],limit[16],position[0],remaining[16]
[U]: capacity[64],limit[16],position[1],remaining[15]
[s]: capacity[64],limit[16],position[2],remaining[14]
[i]: capacity[64],limit[16],position[3],remaining[13]
[n]: capacity[64],limit[16],position[4],remaining[12]
[g]: capacity[64],limit[16],position[5],remaining[11]
[B]: capacity[64],limit[16],position[6],remaining[10]
[u]: capacity[64],limit[16],position[7],remaining[9]
[f]: capacity[64],limit[16],position[8],remaining[8]
[f]: capacity[64],limit[16],position[9],remaining[7]
[e]: capacity[64],limit[16],position[10],remaining[6]
[r]: capacity[64],limit[16],position[11],remaining[5]
[s]: capacity[64],limit[16],position[12],remaining[4]
[]: capacity[64],limit[16],position[13],remaining[3]
[]: capacity[64],limit[16],position[14],remaining[2]
[]: capacity[64],limit[16],position[15],remaining[1]
[]: capacity[64],limit[16],position[16],remaining[0]
-------------------------
Buffer Overflow!!
-------------------------
capacity[64],limit[16],position[5],remaining[11]
capacity[64],limit[16],position[8],remaining[8]
capacity[64],limit[16],position[5],remaining[11]
[F]: capacity[64],limit[16],position[6],remaining[10]
[o]: capacity[64],limit[16],position[7],remaining[9]
[o]: capacity[64],limit[16],position[8],remaining[8]
[f]: capacity[64],limit[16],position[9],remaining[7]
[e]: capacity[64],limit[16],position[10],remaining[6]
[r]: capacity[64],limit[16],position[11],remaining[5]
[s]: capacity[64],limit[16],position[12],remaining[4]
[]: capacity[64],limit[16],position[13],remaining[3]
[]: capacity[64],limit[16],position[14],remaining[2]
[]: capacity[64],limit[16],position[15],remaining[1]
[]: capacity[64],limit[16],position[16],remaining[0]
-------------------------
capacity[64],limit[64],position[0],remaining[64]
capacity[64],limit[6],position[0],remaining[6]
[B]: capacity[64],limit[6],position[1],remaining[5]
[a]: capacity[64],limit[6],position[2],remaining[4]
[r]: capacity[64],limit[6],position[3],remaining[3]
[B]: capacity[64],limit[6],position[4],remaining[2]
[u]: capacity[64],limit[6],position[5],remaining[1]
[z]: capacity[64],limit[6],position[6],remaining[0]
-------------------------

では、エッセンス部分を個別に見ていきます。dumpCharBuffer()は解説不要ですよね?現在位置から1文字get()して(現在位置は1つずれます)、remainingがある間ぐるぐる1文字ずつ出力していくだけです。BufferUtil.dump()とか、dumpCharBuffer()は以降省略したコードで見ていきます。

初期化時点でのcapacityとかlimit、またput()後のposition, rewind()動作の確認

まずCharBufferの初期化部分を見てみます。

// (1) 64バイト分のcharバッファを確保する。
CharBuffer cb = CharBuffer.allocate(64);

// (2) put/getは16char分に制限する。
cb.limit(16);

// (3) charデータを現在のpositionから追記する。
cb.put("UsingBuffers".toCharArray());

// (4) positionを0にしてmarkも破棄する。
cb.rewind();

この結果を見てみます。

capacity[64],limit[64],position[0],remaining[64]
... (1)の時点です。capacity = limit ですね。

capacity[64],limit[16],position[0],remaining[16]
... (2)でlimit(16)をした時点です。limitが16になってます。また、remaining = limitです。

capacity[64],limit[16],position[12],remaining[4]
... (3) で、put()した時点です。positionが12に増え、remainingは limit - positionになります。

capacity[64],limit[16],position[0],remaining[16]
... (4) rewind()するとlimitの値はそのままで、positionを0に戻します。

... ↓で、この状態でぐるぐる回すとバッファの先頭から順に表示されていきます。
[U]: capacity[64],limit[16],position[1],remaining[15]
[s]: capacity[64],limit[16],position[2],remaining[14]
[i]: capacity[64],limit[16],position[3],remaining[13]
[n]: capacity[64],limit[16],position[4],remaining[12]
[g]: capacity[64],limit[16],position[5],remaining[11]
[B]: capacity[64],limit[16],position[6],remaining[10]
[u]: capacity[64],limit[16],position[7],remaining[9]
[f]: capacity[64],limit[16],position[8],remaining[8]
[f]: capacity[64],limit[16],position[9],remaining[7]
[e]: capacity[64],limit[16],position[10],remaining[6]
[r]: capacity[64],limit[16],position[11],remaining[5]
[s]: capacity[64],limit[16],position[12],remaining[4]
... バッファの内容はデフォルトで0ですね。
[]: capacity[64],limit[16],position[13],remaining[3]
[]: capacity[64],limit[16],position[14],remaining[2]
[]: capacity[64],limit[16],position[15],remaining[1]
[]: capacity[64],limit[16],position[16],remaining[0]
-------------------------

position = limitの状態でput()したらjava.nio.BufferOverflowException発生

try {
    // この段階でposition = limitになるため、
    // これ以上のputはBufferOverflowExceptionを発生させる。 
    cb.put("Foo".toCharArray());
} catch (java.nio.BufferOverflowException ignored) {
    System.out.println("Buffer Overflow!!");
}

出力:

Buffer Overflow!!
-------------------------

position()で読み書き位置を設定、mark()とreset()の挙動確認。

// (5)-a 次の読み書き位置を0始まりの5に設定
cb.position(5);
// (5)-b reset()用に現在位置をマーキング
cb.mark();

// (6) 現在位置から"Foo"をバッファに書き込む。
cb.put("Foo".toCharArray());

// (7) 最後にmarkした位置までpositionを戻す。
cb.reset();
capacity[64],limit[16],position[5],remaining[11]
... (5)-a,b 終了後。positionが指定したとおりになっています。
ちょうど"Using"の"g"の位置が"4"文字目になるので、
positionが5と言う事はその次の"B"に次にput()する1文字目が書き込まれます。

capacity[64],limit[16],position[8],remaining[8]
... (6) 終了時点です。3文字分positionが前進しています。

capacity[64],limit[16],position[5],remaining[11]
... (7) の時点です。positionがput()する前の位置に戻ってます。

... ↓この時点でぐるぐる回すと、現在位置がちょうど"F"になるのでそこから表示されます。
[F]: capacity[64],limit[16],position[6],remaining[10]
[o]: capacity[64],limit[16],position[7],remaining[9]
[o]: capacity[64],limit[16],position[8],remaining[8]
[f]: capacity[64],limit[16],position[9],remaining[7]
[e]: capacity[64],limit[16],position[10],remaining[6]
[r]: capacity[64],limit[16],position[11],remaining[5]
[s]: capacity[64],limit[16],position[12],remaining[4]
[]: capacity[64],limit[16],position[13],remaining[3]
[]: capacity[64],limit[16],position[14],remaining[2]
[]: capacity[64],limit[16],position[15],remaining[1]
[]: capacity[64],limit[16],position[16],remaining[0]
-------------------------

ちなみにこの段階でrewind()してぐるぐる回すと、当然"UsingFoofers"という表示になります。実験してみて下さい。

clear()とflip()の挙動確認

このバッファをフリップ (反転) します。リミットは現在位置の値に設定され、現在位置を表す値は 0 に設定されます。マークが定義されている場合、そのマークは破棄されます。
チャネル読み込み操作 (put) のあと、このメソッドを呼び出してチャネル書き込み操作 (相対「get」) の準備を行います。

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle

flip()というのが上記javadocの解説からは全くイメージが湧かなかったので使ってみました。

// (8) limitをcapacityに合わせ、positionも0に戻す。
cb.clear();

// "BarBuz"と先頭から書き込み。
cb.put("BarBuz".toCharArray());

// (9) 書き込んだ所までをlimitにして(ここがrewind()と異なる所)、
// positionを0に戻す。
cb.flip();
capacity[64],limit[64],position[0],remaining[64]
... (8)の時点。確かにcapacity = limitとなり、positionも0になっている。

capacity[64],limit[6],position[0],remaining[6]
... (9)の時点。"BarBuz"まで書き込んだ時点で、positionは0始まりの"6"になる。
そこがlimitに設定され、positionは0に巻き戻される。

... ↓そこからぐるぐるすれば、"ちょうどputされた分だけ"getできる。
[B]: capacity[64],limit[6],position[1],remaining[5]
[a]: capacity[64],limit[6],position[2],remaining[4]
[r]: capacity[64],limit[6],position[3],remaining[3]
[B]: capacity[64],limit[6],position[4],remaining[2]
[u]: capacity[64],limit[6],position[5],remaining[1]
[z]: capacity[64],limit[6],position[6],remaining[0]
-------------------------

flip()というのは、Bufferに書き込まれた分だけちょうど取得したい時に呼んでおく、という使い方が出来るようです。

まとめと雑感

Cとかだと、配列を用意していろんな「今どこ」を管理する必要があるのですが、JavaですとBufferでclear()とかflip()とかrewind()を用いる事で、その辺りを手計算する必要がないように省力化してくれている・・・という感じを受けました。

そもそもいきなりjava.nio.Bufferに手を出したのは、channelを使ったソケット入出力のexamplesでByteBufferやCharBufferのflip()やrewind()が出てきて、「これ、何?」と全然分からなかったのが原因です。
確かにソケット入出力では(C言語でも)バッファに対して何バイト分read/writeして云々というのが必要です。
java.nio.Bufferのメソッドを上手く使うと、その当たりを「数字の手計算無しに」できるのがメリットかなぁ・・・と思いました。
慣れてくれば意外と重宝するクラス群ではないでしょーか・・・。