[Java][文字コード] Java8 の非互換な文字コード変換

文字コード関連でちょっと調べていたら、未定義文字を変換したときの挙動が Java8 と Java7 までで異なることを発見したのでメモ。

Java7 まではネイティブコードの1文字が概ね U+FFFD 1つに変換される。

# 半角の未定義文字
1B284920 -> FF60 (ISO-2022-JP)
8EA0 -> FFFD (EUC-JP)
A0 -> FFFD (Shift_JIS)
A0 -> FFFD (windows-31j)
A0 -> FFFD (x-SJIS_0213)
# 半角の定義文字(参考)
1B284921 -> FF61 (ISO-2022-JP)
8EA1 -> FF61 (EUC-JP)
A1 -> FF61 (Shift_JIS)
A1 -> FF61 (windows-31j)
A1 -> FF61 (x-SJIS_0213)
# 全角の範囲外領域
1B24422120 -> FFFD (ISO-2022-JP)
A1A0 -> FFFD (EUC-JP)
813F -> FFFD (Shift_JIS)
813F -> FFFD (windows-31j)
813F -> FFFD003F (x-SJIS_0213)
# 全角の定義文字(参考)
1B24422121 -> 3000 (ISO-2022-JP)
A1A1 -> 3000 (EUC-JP)
8140 -> 3000 (Shift_JIS)
8140 -> 3000 (windows-31j)
8140 -> 3000 (x-SJIS_0213)
# 全角の未定義文字
1B24422271 -> FFFD (ISO-2022-JP)
A2F1 -> FFFD (EUC-JP)
81EF -> FFFD (Shift_JIS)
81EF -> FFFD (windows-31j)
81EF -> 2194 (x-SJIS_0213)
# 全角の未定義文字
1B24422321 -> FFFD (ISO-2022-JP)
A3A1 -> FFFD (EUC-JP)
8240 -> FFFD (Shift_JIS)
8240 -> FFFD (windows-31j)
8240 -> 25B7 (x-SJIS_0213)
# 全角の未定義文字
1B2442247E -> FFFD (ISO-2022-JP)
A4FE -> FFFD (EUC-JP)
82FC -> FFFD (Shift_JIS)
82FC -> FFFD (windows-31j)
82FC -> FFFD (x-SJIS_0213)

# ISO-2022-JP の最初の変換はバグっぽいですが。

ところが、Java8 だと U+FFFD が1つだったり2つだったり、2バイト目が有効な文字だったら変換されたりと安定しない。

# 半角の未定義文字
1B284920 -> FF60 (ISO-2022-JP)
8EA0 -> FFFD (EUC-JP)
A0 -> FFFD (Shift_JIS)
A0 -> FFFD (windows-31j)
A0 -> FFFD (x-SJIS_0213)
# 半角の定義文字(参考)
1B284921 -> FF61 (ISO-2022-JP)
8EA1 -> FF61 (EUC-JP)
A1 -> FF61 (Shift_JIS)
A1 -> FF61 (windows-31j)
A1 -> FF61 (x-SJIS_0213)
# 全角の範囲外領域
1B24422120 -> FFFD (ISO-2022-JP)
A1A0 -> FFFD (EUC-JP)
813F -> FFFD003F (Shift_JIS)
813F -> FFFD003F (windows-31j)
813F -> FFFD003F (x-SJIS_0213)
# 全角の定義文字(参考)
1B24422121 -> 3000 (ISO-2022-JP)
A1A1 -> 3000 (EUC-JP)
8140 -> 3000 (Shift_JIS)
8140 -> 3000 (windows-31j)
8140 -> 3000 (x-SJIS_0213)
# 全角の未定義文字
1B24422271 -> FFFD (ISO-2022-JP)
A2F1 -> FFFD (EUC-JP)
81EF -> FFFD (Shift_JIS)
81EF -> FFFD (windows-31j)
81EF -> 2194 (x-SJIS_0213)
# 全角の未定義文字
1B24422321 -> FFFD (ISO-2022-JP)
A3A1 -> FFFD (EUC-JP)
8240 -> FFFD0040 (Shift_JIS)
8240 -> FFFD0040 (windows-31j)
8240 -> 25B7 (x-SJIS_0213)
# 全角の未定義文字
1B2442247E -> FFFD (ISO-2022-JP)
A4FE -> FFFD (EUC-JP)
82FC -> FFFD (Shift_JIS)
82FC -> FFFDFFFD (windows-31j)
82FC -> FFFD (x-SJIS_0213)

未定義文字なので大きな問題にはならないかもしれないけれど、こういう非互換は勘弁してほしいなぁ…。

参考までに、チェックに使った環境とソースコードは以下の通り。

java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
java version "1.7.0_72"
Java(TM) SE Runtime Environment (build 1.7.0_72-b14)
Java HotSpot(TM) Client VM (build 24.72-b04, mixed mode)
import java.nio.charset.Charset;

class CheckUndefined {

    static final Charset ISO_2022_JP = Charset.forName("ISO-2022-JP");
    static final Charset EUC_JP = Charset.forName("EUC-JP");
    static final Charset SHIFT_JIS = Charset.forName("Shift_JIS");
    static final Charset WINDOWS_31J = Charset.forName("Windows-31J");
    static final Charset SHIFT_JIS_2004 = Charset.forName("x-SJIS_0213");

    public static void main(String[] args) {
        checkA0();
        checkA1();
        check813F();
        check8140();
        check81EF();
        check8240();
        check82FC();
    }

    static void checkA0() {
        println("# 半角の未定義文字");
        check(new byte[] {0x1B, 0x28, 0x49, 0x20}, ISO_2022_JP);
        check(new byte[] {(byte) 0x8E, (byte) 0xA0}, EUC_JP);
        check(new byte[] {(byte) 0xA0}, SHIFT_JIS);
        check(new byte[] {(byte) 0xA0}, WINDOWS_31J);
        check(new byte[] {(byte) 0xA0}, SHIFT_JIS_2004);
    }

    static void checkA1() {
        println("# 半角の定義文字(参考)");
        check(new byte[] {0x1B, 0x28, 0x49, 0x21}, ISO_2022_JP);
        check(new byte[] {(byte) 0x8E, (byte) 0xA1}, EUC_JP);
        check(new byte[] {(byte) 0xA1}, SHIFT_JIS);
        check(new byte[] {(byte) 0xA1}, WINDOWS_31J);
        check(new byte[] {(byte) 0xA1}, SHIFT_JIS_2004);
    }

    static void check813F() {
        println("# 全角の範囲外領域");
        check(new byte[] {0x1B, 0x24, 0x42, 0x21, 0x20}, ISO_2022_JP);
        check(new byte[] {(byte) 0xA1, (byte) 0xA0}, EUC_JP);
        check(new byte[] {(byte) 0x81, 0x3F}, SHIFT_JIS);
        check(new byte[] {(byte) 0x81, 0x3F}, WINDOWS_31J);
        check(new byte[] {(byte) 0x81, 0x3F}, SHIFT_JIS_2004);
    }

    static void check8140() {
        println("# 全角の定義文字(参考)");
        check(new byte[] {0x1B, 0x24, 0x42, 0x21, 0x21}, ISO_2022_JP);
        check(new byte[] {(byte) 0xA1, (byte) 0xA1}, EUC_JP);
        check(new byte[] {(byte) 0x81, 0x40}, SHIFT_JIS);
        check(new byte[] {(byte) 0x81, 0x40}, WINDOWS_31J);
        check(new byte[] {(byte) 0x81, 0x40}, SHIFT_JIS_2004);
    }

    static void check81EF() {
        println("# 全角の未定義文字");
        check(new byte[] {0x1B, 0x24, 0x42, 0x22, 0x71}, ISO_2022_JP);
        check(new byte[] {(byte) 0xA2, (byte) 0xF1}, EUC_JP);
        check(new byte[] {(byte) 0x81, (byte) 0xEF}, SHIFT_JIS);
        check(new byte[] {(byte) 0x81, (byte) 0xEF}, WINDOWS_31J);
        check(new byte[] {(byte) 0x81, (byte) 0xEF}, SHIFT_JIS_2004);
    }

    static void check8240() {
        println("# 全角の未定義文字");
        check(new byte[] {0x1B, 0x24, 0x42, 0x23, 0x21}, ISO_2022_JP);
        check(new byte[] {(byte) 0xA3, (byte) 0xA1}, EUC_JP);
        check(new byte[] {(byte) 0x82, 0x40}, SHIFT_JIS);
        check(new byte[] {(byte) 0x82, 0x40}, WINDOWS_31J);
        check(new byte[] {(byte) 0x82, 0x40}, SHIFT_JIS_2004);
    }

    static void check82FC() {
        println("# 全角の未定義文字");
        check(new byte[] {0x1B, 0x24, 0x42, 0x24, 0x7E}, ISO_2022_JP);
        check(new byte[] {(byte) 0xA4, (byte) 0xFE}, EUC_JP);
        check(new byte[] {(byte) 0x82, (byte) 0xFC}, SHIFT_JIS);
        check(new byte[] {(byte) 0x82, (byte) 0xFC}, WINDOWS_31J);
        check(new byte[] {(byte) 0x82, (byte) 0xFC}, SHIFT_JIS_2004);
    }

    static void check(byte[] bytes, Charset charset) {
        println(String.format("%s -> %s (%s)",
                toHexString(bytes),
                toHexString(new String(bytes, charset).toCharArray()),
                charset.toString()));
    }

    static String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte c : bytes) {
            sb.append(String.format("%02X", c));
        }
        return sb.toString();
    }

    static String toHexString(char[] chars) {
        StringBuilder sb = new StringBuilder();
        for (char c : chars) {
            sb.append(String.format("%04X", (int) c));
        }
        return sb.toString();
    }

    static void println(String s) {
        System.out.println(s);
    }

}