【Java】CharsetEncoderを利用した、文字列のカット
#Java #UTF-8 #文字コード
Qiita-Java 文字列をバイト数で切り捨てる
上記の記事に掲載されている、
code:java
public String truncateBytes(String s, Charset charset, int maxBytes) {
ByteBuffer bb = ByteBuffer.allocate(maxBytes);
CharBuffer cb = CharBuffer.wrap(s);
CharsetEncoder encoder = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE)
.reset();
CoderResult cr = encoder.encode(cb, bb, true);
if (!cr.isOverflow()) {
return s;
}
encoder.flush(bb);
return cb.flip().toString();
}
このコード。
文字列を、指定されたバイト数以下になるように切り捨てるコードらしいが、自身の知識不足&各処理に対する説明があまりないことから、何をやっているのかさっぱりだった。
1つずつ確認していくと「あーなるほど」という感じで納得できたので、忘れないうちにメモ。
やっていること
記事内にも書かれている通り、
CharsetEncoder#encodeでバイト配列に変換してみるけど、バイト配列に収まらないところでストップするので、
このとき進んだchar配列分を文字列に変換する。ってイメージ。
のまんま。(そらそうだけど)
簡易的に文字列操作ができるAPIがNIOパッケージに存在しており、今回はそのAPI郡を利用している。
コードリーディング
code:java
public String truncateBytes(String s, Charset charset, int maxBytes) {
// 1. 入力文字を CharBuffer でラップ。
// 内部的には StringCharBuffer が作成されている。
final CharBuffer cb = CharBuffer.wrap(s);
// 2. maxByte 数分のバッファ領域をもつ ByteBuffer を作成する。
final ByteBuffer bb = ByteBuffer.allocate(maxBytes);
// 3. 指定された文字コードのエンコーダーを作成する。
// CharsetEncoderを扱う際に気をつけなければならないことは以下
// 1. encodeメソッドを呼び出す前に、必ず reset メソッドを呼び出すこと。(ただし、初回は不要)
// 2. 追加入力がある場合は、encodeメソッドの引数 endOfInput に false を指定すること。
// 3. 追加入力がない場合は、encodeメソッドの引数 endOfInput に true を指定すること。
// 4. 最後は flash メソッドを呼び出し、内部状態を出力バッファへフラッシュさせること。
// 5. CharsetEncoderはスレッドセーフではない。
final CharsetEncoder encoder = charset.newEncoder()
// 入力形式が正しくないエラーが発生した場合の挙動(指定文字で置き換える)
.onMalformedInput(CodingErrorAction.REPLACE)
// マッピング不可文字エラーが発生した場合の挙動(指定文字で置き換える)
.onUnmappableCharacter(CodingErrorAction.REPLACE)
// 置き換え文字の設定(デフォルトは '?' なので、実はこの設定は意味がない)
.replaceWith(new byte[]{(byte) '?'})
// UTF-8の場合は reset メソッドの中身は空なので意味はない。
.reset();
// 入力文字をエンコードし、ByteBuffer に結果を書き込む。
// エンコードの結果、ByteBufferのバッファ領域を超えていない場合は、入力文字をそのまま戻す。
final CoderResult cr = encoder.encode(cb, bb, true);
if (!cr.isOverflow()) {
return s;
}
// 最大バイト数を超えている場合、以下の順で最大バイト数以下の文字を取得する。
// 1. 出力バッファへフラッシュ(flushメソッド)
// 2. 入力バッファ側のリミットを現在の読み込み位置に設定後、現在位置を0に設定。(flipメソッド)
// 3. 現在位置からリミットまでのバッファを文字列に変換。(toStringメソッド)
encoder.flush(bb);
return cb.flip().toString();
}
個人的にミソだと思うのは最後のCharBuffer#flipを呼び出し後に、文字列化している点。
flipメソッドは、バッファが内部的に保持している「現在位置」をバッファの「末尾位置」として設定後に、「現在位置」を0に設定する。(flipメソッドはCharBufferのメソッドではなく、親の Buffer で定義されているメソッド)
で、この「現在位置」というのはバッファから読み込んだ際に「どこまで読み込み終えたか」を表すインデックスのこと。
上記コードのCharsetEncoder#encodeでは、入力バッファ(CharBuffer)から読み込んで、出力バッファ(ByteBuffer)へ書き出すという処理を行っており、その際に入力バッファ内の「現在位置」は変動している。
CharsetEncoder#encodeでは、大まかに以下の処理を行っている。
1. CharsetEncoder側で、入力バッファの「現在位置」(インデックス)を保持。
2. 入力バッファから読み込み。
3. 読み込んだデータを、出力バッファに書き込み。
4. 出力バッファ側の容量を超えてしまった場合、入力バッファ側の「現在位置」を1で保持しておいた値で更新。
5. 入力バッファから読み込むデータが無くなるか、エラーが発生するまで1 ~ 4を繰り返す。
6. 呼び出し元に処理を戻す。
仮に出力バッファへ書き込めなかった(=指定されたバイト数よりも大きいデータ量の文字列だった)場合、入力バッファ側の「現在位置」はエラーが発生する直前にマーキングした位置ということになる。
そのためflipメソッドが呼ばれると、バッファ内の「末尾位置」が「現在位置」に置き換わるため、toStringメソッドはデータが溢れる直前までのデータをStringに変換してくれる。
StandardCharsets.UTF_8から生成するCharsetEncoderは、サロゲートペアが考慮されている
StandardCharsets.UTF_8から生成されるCharsetEncoder はサロゲートペアが考慮されており、toStringが呼ばれたときに文字が壊れることはない。(壊れる=生成した文字列内に?が含まれる等)
参考にしたサイト
http://www.techscore.com/tech/Java/JavaSE/NIO/4-3/
https://qiita.com/ota-meshi/items/16972156c935b8b7feaa
https://www.ibm.com/developerworks/jp/ysl/library/java/j-unicode_surrogate/
https://qiita.com/deco/items/81338e744945f9fef5b2
http://www.yts.rdy.jp/java/surrogate.html