レビューで鍛える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ファイル出力機能などと連携したり
携帯向け情報画面を別途作成するような場合に、ちょくちょく発生する要件ですね。
コラム
文字コードとエンコードについては、
私自身、いくつかの案件でハマったり、学んだりしてきたのですが、
いまだに新しい問題を見つけてしまうことがある、一筋縄ではいかない問題だと思います。
っていうか、そもそも分かりづらいですよね?
・「文字コード」と「エンコード」の区別がつかない
・「ユニコード」の言葉の意味が実はよく分からない
・「文字コード」と「エンコード」の間違いにツッコミを入れたら「細かい」って言われてしまった
・JavaのUnicode変換表と、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-31j → Unicode → Windows-31j」の変換の過程で
勝手に文字コードが変わってしまう文字があります。
http://support.microsoft.com/default.aspx?scid=kb;ja;JP170559
さらに「Unicode → Windows-31j → Unicode」の変換の過程で
勝手に文字そのものが変わってしまう文字もあるのです。
http://www.bugbearr.jp/?Java/%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89
このページの「Javaでのラウンドトリップ問題」に、その文字の一覧があります。
こうした現象がなぜ起きるのかは、今回は割愛しますが
対応としては、
「Windows-31j → Unicode → Windows-31」のラウンドトリップ問題は無視してよく、
「Unicode → Windows-31j → Unicode」のラウンドトリップ問題には、注意を払う必要があります。
では、ラウンドトリップ問題が起きる文字列が
どのように変換されるのか、また表にまとめてみましょう。
元の文字列 | 元の文字列の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 |
注意を払うべきと書いた「Unicode → Windows-31j → Unicode」については、エラーとしています。
多少悲観的な方法ではありますが、エラーとしておくのが無難でしょう。
エラーとする以外には、問題が起きる文字列を
「事前にWindows-31jの文字列に変換する」という対策がありますが、ここでは割愛します。
まとめ
それでは、今回の問題のまとめです。
- 文字コードチェックは「getBytes → new String」した結果が、元の文字列と一致するかで判定できる
- 「Windows-31j → Unicode → Windows-31j」でラウンドトリップしない文字列には実害がないので、無視して良い。
- 「Unicode → Windows-31j → Unicode」でラウンドトリップしない文字列は、エラーにするか、変換する必要がある。
今回の問題は、文字コードについて少し詳しい方でないと、分かりづらかったかも知れません。
いずれ改めて、文字コード、エンコードなどについて説明したエントリを作成する予定ですので
そちらを併せて確認してください。