Javaにおける文字コードまわりの話(2)

Javaにおける文字コードまわりの話 - あしのあしあと」は、もう少しブラッシュアップしたい。その前に、検証用のプログラムを少しだけ整理しておきたい。

ここでは、次のような用語を用いることにする。

  • 文字と文字の識別子の集合を「文字集合」と呼び、文字の識別子を「コードポイント」と呼ぶ。
  • コードポイントからバイト列(バイト配列)へ変換する処理を「エンコード」と呼び、その逆を「デコード」と呼ぶ。
  • エンコード、デコードの方法を「エンコード方式」や「文字エンコーディング」と呼ぶ。




Javaでは、文字集合と文字エンコーディングを組み合わせた「エンコーディングセット」という概念が用いられる。「エンコーディングセット」って用語、正直、あまり使わない*1。。
http://java.sun.com/javase/ja/6/docs/ja/technotes/guides/intl/encoding.doc.html

サポートされているエンコーディングセット名の一覧は、次のようにして出力できる。

/**
 * 現在のJava仮想マシンでサポートされているエンコーディングセット名を出力する。
 */
public static void printSupportedCharSet() {
    println(String.format("この環境のデフォルトエンコーディングセットは%sです。",
            Charset.defaultCharset().name()));

    println("この環境でサポートしているエンコーディングセットは次です。");
    println("書式: java.nio API用の正準名 ( 別名... )");
    Map<String, Charset> charSetMap = Charset.availableCharsets();
    for (Map.Entry<String, Charset> entry : charSetMap.entrySet()) {
        print(String.format("%s ( ", entry.getKey()));
        for (String alias : entry.getValue().aliases()) {
            print(alias + " ");
        }
        println(")");
    }
}
private static void println(Object obj) { System.out.println(obj); }
private static void print(Object obj) { System.out.print(obj); }

さて、Javaの内部では、文字は、Unicodeを用いて扱われている。Java6では、BMP基本多言語面)以外の文字にも対応しているが、今回もBMPの文字のみの話とする。
char型は、int型にキャストできる*2。この整数値が、Unicodeのコードポイントそのものだったりする。

int ch = (int) 'あ';

この場合であれば、次でも同じ。

char[] chars = {'あ'};
int ch = Character.codePointAt(chars, 0);

この時chの値は12354(0x3042)。ちょっとダサいが、これを16進数で表示するプログラムを書いてみた。

/**
 * 例えば、引数の文字として{@code a} を指定した場合には、{@code U+0061} が返される。
 * @param ch BMP(基本多言語面)に含まれる文字
 * @return 指定された文字のUnicodeコードポイント文字列
 */
public static String getUnicodeCodePointString(char ch) {
    if (!Character.isDefined(ch))
        throw new IllegalArgumentException(
            String.format(
                "BMP(基本多言語面)に含まれていない文字 %s(%s) が指定されました。",
                ch, (int) ch));

    String hexCp = Integer.toHexString((int) ch);

    // 以下で、桁数を BMP_HEX_LENGTH 桁にそろえる。また、先頭に U+ を付加する。
    // 例えば、hexCp が 61 の場合に U+0061 にする。

    final int BMP_HEX_LENGTH = 4;
    if (BMP_HEX_LENGTH < hexCp.length())
        // スローされないはず。
        throw new RuntimeException(String.format(
            "文字 %s の16進数表現 %s が、%s 桁以内になっていません。",
            ch, hexCp, BMP_HEX_LENGTH));

    StringBuilder sb = new StringBuilder();
    while (sb.length() + hexCp.length() < BMP_HEX_LENGTH) {
        sb.append('0');
    }
    sb.append(hexCp.toUpperCase());

    return "U+" + sb.toString();
}

これで、Javaの内部で扱われている文字が、どのUnicodeのコードポイントにマッピングされているのか、調査することができる。
では、エンコーディングセットに含まれるすべての文字を取得しよう。文字(コードポイントの値)をインクリメントしていって、CharsetEncoder#canEncodeメソッドでエンコードできる文字を追加しているだけ。ちなみにメソッド中で、CharsetやCharsetEncoderを構築しているので、何度も呼ばないこと。TreeSetを用いているのは、表示のため。検索するならHashSetを用いること。

/** コードポイント順にソートするコンパレータ */
private static final Comparator<Character> CHAR_VALUE_COMPARATOR =
    new Comparator<Character>() {
    @Override
    public int compare(Character ch1, Character ch2) {
        return ch1.charValue() - ch2.charValue();
    }
};

/**
 * @param encordingsetName エンコーディングセット名
 * @return 指定されたエンコーディングセット名に含まれるすべての文字
 */
public static Set<Character> getAllChars(String encordingsetName) {
    Charset charset = Charset.forName(encordingsetName);
    CharsetEncoder encoder = charset.newEncoder();
    Set<Character> chars = new TreeSet<Character>(CHAR_VALUE_COMPARATOR);

    char ch = Character.MIN_VALUE;
    for (; ch < Character.MAX_VALUE; ch++) {
        if (encoder.canEncode(ch)) {
            chars.add(Character.valueOf(ch));
        }
    }
    return chars;
}

余談だが、java.nioのエンコーダやデコーダは、次のように使える。これもCharset、CharsetEncoder、CharsetDecoderを毎回構築しているし、CharBufferやByteBufferを毎回確保しているので、調査目的以外では用いないこと。

/**
 * @param encordingSetName エンコーディングセット名
 * @param chars エンコード対象の文字配列
 * @return 指定された文字配列をエンコードしたバイト配列
 * @throws CharacterCodingException エンコードに失敗した場合にスローされる
 * @see CharsetEncoder#encode(CharBuffer)
 */
public static byte[] encordChars(String encordingSetName, char... chars)
throws CharacterCodingException {
    Charset charset = Charset.forName(encordingSetName);
    CharsetEncoder encoder = charset.newEncoder();
    CharBuffer in = CharBuffer.wrap(chars);
    ByteBuffer out = encoder.encode(in);
    return out.array();
}

/**
 * @param encordingSetName エンコーディングセット名
 * @param bytes デコード対象のバイト配列
 * @return 指定されたバイト配列をデコードした文字配列
 * @throws CharacterCodingException デコードに失敗した場合にスローされる
 * @see CharsetDecoder#decode(ByteBuffer)
 */
public static char[] decodeBytes(String encordingSetName, byte... bytes)
throws CharacterCodingException {
    Charset charset = Charset.forName(encordingSetName);
    CharsetDecoder decoder = charset.newDecoder();
    ByteBuffer in = ByteBuffer.wrap(bytes);
    CharBuffer out = decoder.decode(in);
    return out.array();
}

ここまでで、次のことができるようになった。

今日はここまで。続きはいつできることやら。。

2010/07/24追記

Javaにおける文字コードまわりの話(5)」に、(整理し切れてはいないのだが)簡単なまとめを書いた。

*1:java.nio APIであれば、エンコーディングセットの実体は、java.nio.charset.Charsetのサブクラスである。この場合は「文字セット」という用語が用いられる。こっちの方が一般的な気がする。

*2:IT素人で、いきなりこれを見たときには、ぎょっとしたw