【Java】UTF-8(BOM付き)が0xFFFD(REPLACEMENT CHARACTER)に変換される

# はじめに
システム改修(Java7)で、FTP受信したファイルで行の先頭文字が「K」になっている行数を数える処理を作成した。行の先頭文字が「K」かどうかは正規表現「^K.*」を使用している。

テストではShift-JISとして受信ファイルを作成していたため、FTP受信したファイルの読み込みの文字コードの指定は「Windows31J」で問題がなかったが、実際のFTP受信したファイルはUTF-8(BOM付き)であったことから、BOMにより先頭行だけ「K」が判定されず、行数の総数が合わなくなった。

仕様の見落としであった。

# BOM
BOMとは「Byte Order Mark([バイトオーダーマーク](https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%83%88%E3%82%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%83%9E%E3%83%BC%E3%82%AF))」の略で、「このファイルは Unicode 形式で書かれています」と判別させるための情報である。

主な文字コードとBOMのみ抜粋します。

|文字コード|エンディアンの区別|BOMコード|
|:--|:--|:--|
|UTF-8|	|0xEF 0xBB 0xBF|
|UTF-16|BE|0xFE 0xFF
|      |LE|0xFF 0xFE

# BOMスキップ
下記サイトのコードを参考にしてBOMをスキップする処理を追加したが、結果は変わらない。
[【JAVA】BOM（Byte Of Mark）の手動除去ロジック](http://rmrmrmarmrmrm.seesaa.net/article/401632047.html)

``` java
private static String excludeBOMString(String original_str) {
	if (original_str != null) {
		char c = original_str.charAt(0);
		if (Integer.toHexString(c).equals("feff")) {
			StringBuilder sb = new StringBuilder();
			for (int i=1; i < original_str.length(); i++) {
				sb.append(original_str.charAt(i));
			}
			return sb.toString();
		} else {
			return original_str;
		}
	} else {
		return "";
	}
}
``` 
何で？と思って自PCのWindows7のEclipse上でデバッグすると、先頭文字が"fffd"になっていたのである。もう少し文字コードを調べていくと下記のようになっており、先頭文字の「K(0x4b)」まで3文字スキップすればいいと分かった。
UTF-8のBOMコード(0xEF,0xBB,0xBF)だし、Javaの内部コードはUTF-16LEでBOMコード(0xfeff)とは違うと思いつつも、午後の検証作業まで時間がなかったので、とりあえず解明するのは後回しにした。

|Index|文字コード|
|:--|:--|
|0|0xfffd|
|1|0xff7b|
|2|0xff7f|
|3|0x4b|

BOM判定は「fffd」とし、スキップは3文字として修正した。これで正常にBOMをスキップでき、行の先頭文字が「K」の総数は正しく取得できるようになった。

``` java
private static String excludeBOMString(String original_str) {
	if (original_str != null) {
		char c = original_str.charAt(0);
		if (Integer.toHexString(c).equals("fffd")) {
			StringBuilder sb = new StringBuilder();
			for (int i=3; i < original_str.length(); i++) {
				sb.append(original_str.charAt(i));
			}
			return sb.toString();
		} else {
			return original_str;
		}
	} else {
		return "";
	}
}
```

午後の検証作業で、修正したプログラムを入れ替えたところ、総数が合わなかった。
何で？と思って自PCのWindows7のEclipse上でデバッグさせてもらうと、総数が一致する。
検証環境はWindows Server 2012R2なので、環境によって何か違うのかとデバッグで読み込み行を出力してみると、「K」の次の文字からとなっていたので、スキップを3文字から2文字にすると総数が合うようになった。

しかし、環境によって異なるのは怖いね。

# 0xFFFD(REPLACEMENT CHARACTER)
なにわともあれ午後の検証作業は終わったので原因を調査することになった。

>「0xfffd」については、UTF-8でエンコードされたテキストファイルをShift-JISで読み込もうとした際に、該当する文字が存在しない場合は'0xFFFD'の文字に変換されます。 
 [Javaでの文字化け検出について](http://steavevaivai.hatenablog.com/entry/2017/04/21/051850)

>当方の環境(Windows 7)では…コードナンバー0xFFFDの『REPLACEMENT CHARACTER』こと、�さんですっ！
当方の環境では黒い菱型に白抜きの"？"マークなので、ちゃんと表示されてない？
っと思ったけど、これはまさに『表示できない場合に表示する文字』らしいので、これでいいみたい
 [Unicodeで事実上最大となる文字](http://blog.livedoor.jp/t122/archives/53155861.html?.link_prev=1)


ファイルの読み込みの文字コードの指定は「Windows31J」となっていたため、BOMコード部分が該当する文字が存在しないとして「0xfffd」の文字に変換される仕組みとなっているということである。これについては納得した。

あと、Windows 7とWindows Server 2012R2上でスキップ文字数が違った件について、Windows Server 2012R2上では2番目にあった「0xff7b」が消えていたため、3文字スキップでは行き過ぎることが分かった。

|Index|Win 7文字コード|Index|Win2012 文字コード|
|:--|:--|:--|:--|
|0|0xfffd|0|0xfffd|
|1|0xff7b||
|2|0xff7f|1|0xff7f|
|3|0x4b|2|0x4b

ネットで調べたが、これについては文献が見つからなかった。
この調査に時間を掛けるよりもファイル内の文字は英数字のみとなっているため、ファイルの読み込みの文字コードの指定を「UTF-8」にした方がいいと判断して、プログラムを修正した。

これにより、BOMコードの判定は「0xfeff」でスキップは1文字とした最初の手動除去ロジックのままとなった。
Windows 7とWindows Server 2012R2で検証してどちらも同じ結果となりました。

# 最後に
今回、REPLACEMENT CHARACTER(0xfffd)に変換されるって経験は初めてだったので、備忘録として書いてみました。

JavaでUTF-8といったら、BOMなしのUTF-8 を想定しており、いろいろ議論はあるけれど、後方互換性の問題から対処するつもりはないそうです。
参照:[JavaSE BOM付きUTF8ファイルを読み込むとどうなるか？](https://hondou.homedns.org/pukiwiki/pukiwiki.php?JavaSE%20BOM%C9%D5%A4%ADUTF8%A5%D5%A5%A1%A5%A4%A5%EB%A4%F2%C6%C9%A4%DF%B9%FE%A4%E0%A4%C8%A4%C9%A4%A6%A4%CA%A4%EB%A4%AB%A1%A9)

以前、PHPでもBOM付きにして嵌ったことがありますが、BOMあり/なしは厄介ですね。