JDK1.4で新設されたnew I/O関連に、バッファークラスがある。
でもちょっと癖があって分かりづらい(苦笑)
Bufferの用途・存在意義は、「バッファー」という名前が示す通り、データを保存(追加)し、そこから取得する為のもの。ではあるのだけれど。
Bufferを使う上でややこしいのが、そのBufferインスタンスを今「書き込みに使用しているのか」「読み取りに使用しているのか」をプログラマーが把握・意識して、それに応じた使い方(メソッド呼び出し)をしなければならない(制御する必要がある)こと。
また、同一メソッドでも、読み取りの場合と書き込みの場合で意味合いが異なるものがある。→メソッド一覧
java.nio.Bufferは全てのバッファーの親である抽象クラスであり、データ型に応じて実装クラスがある。
例えばbyteを扱うのはByteBuffer、charを扱うのはCharBuffer。
ただし、実際にはさらにByteBufferやCharBufferを継承したクラス(publicでない)が使われる。
Bufferは、position・limit・capacityというパラメーターを持っている。
capacityは、バッファーサイズ(上限)。不変。
positionは、バッファー内での読み書きをする開始/現在位置。
limitは、書き込みの場合、書き込める最大サイズ(書き込める上限)。読み取りの場合、保持しているデータサイズ(読み込める上限)。
capacityは変わらないが、positionやlimitは読み書き操作に応じて変わっていく。
書き込みも読み取りも、現在のpositionの位置から開始する。
処理内容 | 処理後のバッファー内の様子 | 解説 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | //バッファーインスタンスを生成する |
|
allocate()は バッファーを作成する。 positionは0、 limitはcapacityになる。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
2 | //バッファーへ書き込む |
|
put() positionの位置に書かれ、 書き込んだ分だけ positionが動く。 もしlimitまで到達して なお書き込もうとしたら、 オーバーフロー(例外発生)。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
3 | //バッファーへ書き込む |
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
4 | //読み込み状態に変更する |
|
flip()は、 limitをpositionの値に設定し positionを0にする。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
5 | //バッファーから読み取る |
|
get() positionの位置から読まれ、 読み込んだ分だけ positionが動く。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
6 | //バッファーから読み取る |
|
読取時のhasRemaining()は、 残りデータが有ればtrue。 positoin=limitになるとfalse。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
7 | //バッファーをクリアする |
|
clear()は、 positionを0、 limitをcapacityに戻す。 (データ自体は消されない) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
8 | 2(書き込み開始)へ戻る | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6' | 上記5の直後(データが残っている状態)で//追加書き込みできる状態にする |
|
compact()は 読まれなかったデータを 0の位置に移動させ、 positionを続きの位置に移す。 limitはcapacityに戻す。 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
7' | //バッファーへ書き込む |
|
(データが追加される) |
バッファー作成直後にいきなり読み取ろうと(get)すると、limitはcapacity位置なので、いきなり最大サイズまで読み込むことになってしまう。
また、書き込み(put)し終わった後にflip()せずに読み込もうとすると、positionは書き込んだ直後の位置なのでそこから読み取りをスタートすることになり、書き込んだデータでなく初期化されている(あるいは以前書き込んだ)データをlimit位置(=capacity)まで読み込むことになってしまう。
同様に、全部読み終わった後にclear()せずに書き込もうとすると、positionがlimit位置に居るのでそれ以上書き込めない。 いきなりオーバーフローになってしまう。
読んだ後にclear()でなくflip()したとすると、(limitにはpositionが代入されるので)limitは最後に読み込んだ直後の位置となり、positionは0になる。
この状態で書き込み(put)を始めると、前回読み込んだサイズ(=limit位置)までしか書き込めないという妙な事態になる。
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を返す。 |
文字コードを変換する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); }