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 voiddecode(InputStream is, Charset cs) throws IOException {CharsetDecoderdecoder = cs.newDecoder();booleanend = 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("ファイル名");FileChannelfch = fis.getChannel();try {decodeCh(fch, cs);} finally {fis.close(); // 取得したチャネルもクローズされる}}public static voiddecodeCh(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);}