谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

レビューで鍛えるJavaコーディング力 その7(文字コードチェック)

今回は、文字コードのチェック(エンコーディングチェック)を行う処理に関する問題です。

問題

以下のコードの問題を指摘し、修正してください。
ただし、問題は複数あることもあれば、全くないこともあります。

public class StringValidator {
	private static boolean checkCharacterCode(String str, String encoding) {
		if (str == null) {
			return true;
		}

		try {
			byte[] bytes = str.getBytes(encoding);
			return str.equals(new String(bytes, encoding));
		} catch (UnsupportedEncodingException ex) {
			throw new RuntimeException("エンコード名称が正しくありません。", ex);
		}
	}

	public static boolean isWindows31j(String str) {
		return checkCharacterCode(str, "Windows-31j");
	}

	public static boolean isSJIS(String str) {
		return checkCharacterCode(str, "SJIS");
	}

	public static boolean isEUC(String str) {
		return checkCharacterCode(str, "euc-jp");
	}

	public static boolean isUTF8(String str) {
		return checkCharacterCode(str, "UTF-8");
	}
}


「そもそも文字コードチェックなどする必要があるのか?」なんて
疑問があるかも知れませんが、たとえばWebアプリケーションにおいて、
Web画面以外にも、テキストファイルやPDFファイル出力機能などと連携したり
携帯向け情報画面を別途作成するような場合に、ちょくちょく発生する要件ですね。

コラム

文字コードエンコードについては、
私自身、いくつかの案件でハマったり、学んだりしてきたのですが、
いまだに新しい問題を見つけてしまうことがある、一筋縄ではいかない問題だと思います。


っていうか、そもそも分かりづらいですよね?


 ・「文字コード」と「エンコード」の区別がつかない
 ・「ユニコード」の言葉の意味が実はよく分からない
 ・「文字コード」と「エンコード」の間違いにツッコミを入れたら「細かい」って言われてしまった
 ・JavaUnicode変換表と、Unicodeコンソーシアムの変換表が違う事を知らず、ドツボにハマった
 ・文字コードとかよく分からないんだけど、俺のシステムは、ちゃんと動いてるらしい
 ・実は使われるとダメな文字があるんだけど、ユーザが使ってないから、たまたま助かってる


なんて、一つや二つ、思いあたる所がある方も多いでしょう。


今回の問題や、また別エントリなどを通して
文字コードエンコードの問題の対処方法が分かれば・・・と思います。

解答

さて、今回の答えは「問題なし」です。


今回のソースは、決して万能のチェックとは言えない面もありますが、
ほぼ問題なく、多くの案件で利用できる方法だと思います。


詳しい仕組みなどを説明し始めると、かなり長文になってしまうため
文字コードエンコードUnicodeなどについては、また別にエントリにて説明することにして
今回は、簡単な挙動だけ説明しましょう。

StringValidatorの挙動

StringValidatorのisXxxメソッドを呼び出した際に、
きちんと変換できる文字であれば、、、
 1. getBytes(encoding)を行なうと、その文字列のバイト配列を取得できる。
 2. このバイト配列を使ってnew Stringを行うことで、元の文字列が得られる。
 3. 変換前の文字列と、2で得られた変換後の文字列は、一致する。
という挙動になります。


しかし、指定したエンコードに変換できない場合(対象の文字が存在しない場合)、、、
 1. getBytesすると、変換出きない文字を、「?」に相当する「0x3F」にしたバイト配列が取得される。
 2. このバイト配列を使ってnew Stringすると、一部の文字列が「?」に化けてしまう。
 3. 変換前の文字列と、2で得られた文字列は、「?」の部分が一致しない。
という挙動になります。


と言ってもイメージしにくいので、分かりやすいよう、Windows-31jの場合の変換を表にまとめました。

元の文字列 元の文字列のUnicode getBytes結果 new String結果 変換後のUnicode 判定
\u3042 0x82 0xA0 \u3042 TRUE
あお \u3042 \u304A 0x82 0xA0 0x82 0xA8 あお \u3042 \u304A TRUE
アオ (半角カナ) \uFF71 \uFF75 0xB1 0xB5 アオ \uFF71 \uFF75 TRUE
\u2460 0x87 0x40 \u2460 TRUE
お〜い \u304A \uFF5E \u3044 0x82 0xA8 0x81 0x60 0x82 0xA2 お〜い \u304A \uFF5E \u3044 TRUE
お〜い \u304A \u301C \u3044 0x82 0xA8 0x3F 0x82 0xA2 ? \u304A \u003F \u3044 FALSE


この表から分かるように、
Windows-31jで扱える文字は、機種固有文字含めて「TRUE」となりますが、
「〜」いわゆる「気持ち悪いニョロ」などは、
Windows-31jには対応する文字がないため、「FALSE」となるのです。


つまり「元の文字列が、Windows-31jであるかどうか」という判定が
きちんとできている、ということになります。

ラウンドトリップできない文字はどうか?

ところで、世の中には「Windows-31jUnicodeWindows-31j」の変換の過程で
勝手に文字コードが変わってしまう文字があります。
http://support.microsoft.com/default.aspx?scid=kb;ja;JP170559


さらに「UnicodeWindows-31jUnicode」の変換の過程で
勝手に文字そのものが変わってしまう文字もあるのです。
http://www.bugbearr.jp/?Java/%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89
このページの「Javaでのラウンドトリップ問題」に、その文字の一覧があります。


こうした現象がなぜ起きるのかは、今回は割愛しますが
対応としては、
Windows-31jUnicodeWindows-31」のラウンドトリップ問題は無視してよく、
UnicodeWindows-31jUnicode」のラウンドトリップ問題には、注意を払う必要があります。


では、ラウンドトリップ問題が起きる文字列が
どのように変換されるのか、また表にまとめてみましょう。

元の文字列 元の文字列のUnicode getBytes結果 new String結果 変換後のUnicode 判定
\u2252 0x81 0xE0 \u2252 TRUE
\u221a 0x81 0xE3 \u221a TRUE
« \u00AB 0x81 0xE1 \u226A FALSE
¢ \u00A2 0x81 0x91 ¢ \uFFE0 FALSE


注意を払うべきと書いた「UnicodeWindows-31jUnicode」については、エラーとしています。
多少悲観的な方法ではありますが、エラーとしておくのが無難でしょう。


エラーとする以外には、問題が起きる文字列を
「事前にWindows-31jの文字列に変換する」という対策がありますが、ここでは割愛します。

まとめ

それでは、今回の問題のまとめです。

  • 文字コードチェックは「getBytes → new String」した結果が、元の文字列と一致するかで判定できる
  • Windows-31jUnicodeWindows-31j」でラウンドトリップしない文字列には実害がないので、無視して良い。
  • UnicodeWindows-31jUnicode」でラウンドトリップしない文字列は、エラーにするか、変換する必要がある。


今回の問題は、文字コードについて少し詳しい方でないと、分かりづらかったかも知れません。
いずれ改めて、文字コードエンコードなどについて説明したエントリを作成する予定ですので
そちらを併せて確認してください。