Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

Unicode 4.0 の補助文字のサポート Supplementary Char

 
 
Tiger char で収まらない文字
 
 

ご存知でしたが、Unicode では char では収まらない文字があることを。

Java では言語が発表された当初から内部コードとして Unicode を採用してきました。もちろん、char は Unicode で文字が表される 16 bit になっていました。

だから、Unicode と char は相性がいいはずなのですが、今になって 16 bit では収まりませんでしたといわれても...

Unicode で当初考えられていた文字数よりも地球上で使われる文字が全然多かったというわけですね。16 bit に収まらなかった文字を補助文字 (Supplementary Character) といいます。

補助文字が定義されたのは Unicode 2.0 からのようですが、実際に補助文字が使われたのは 3.1、そして Tiger では Unicode 4.0 をサポートするのです。ということは補助文字をサポートすることは必然です。

この問題に対して Java でどのように補助文字をサポートするかを考えたのが JSR-204 "Java Specification Request for Unicode Supplementary Character Support" です。

Unicode では文字をあらわすのコードポイントという数字を使用します。一般には U を使って U+0001 のようにあらわします。今までの 16 bit で収まっていた文字コードは U+0000 から U+FFFF です。Unicode では文字の塊 (たとえばひらがなとか) を面 (Plain) であらわしますが、この U+0000 から U+FFFF をまとめて基本多言語面 (Basic Multilingual Plane) と呼びます。

これに対して補助文字は U+10000 から U+10FFFF の範囲であらわされます。

そうすると困ったのは文字の表し方です。今までは 16 bit を 8 bit に収めるための UTF-8 と 16 bit 表現をそのまま使用する UTF-16 がありました。ところが UTF-16 をそのまま使うことができなくなってしまったわけです。

そこで、UTF-32 が制定されました。UTF-32 は 32 bit の文字コードをそのまま使用します。UTF-16 で補助文字をあらわすには上位サロゲートと下位サロゲートに分解してあらわすことになります。

たとえば補助文字を 16 進でなく、2 進であらわすと次のようになります。

000u uuuu xxxx xxxx xxxx xxxx

はじめの 3 bit は使用されていないので、0 のままです。残りの 21 bit をまず 5 bit と 16 bit に分割します。

これを次のように分解します。

上位サロゲート         下位サロゲート
1101 10ww wwxx xxxx    1101 11xx xxxx xxxx

ここで wwww は uuuuu - 1 になります。x の部分は上位サロゲートで 6 bit、下位サロゲートで 10 bit をあらわすようになります。

これを 16 進であらわすと上位サロゲートは D800 から DBFF、下位サロゲートは DC00 から DFFF が使われます。もともとこの領域 D800 から DFFF は UTF-16 が使用する部分として文字が割り当てられていないため、このような使い方ができるようです。

たとえば U+10400 は下位 10 bit が 00 0000 0000、なりその上の 6 bit が 00 0001 になります。先頭の 5 bit が 0 0001 なので 1 を引くと 0 0000 となります。したがって、

上位サロゲート 1101 1000 0000 0001 = D8 01

下位サロゲート 1101 1100 0000 0000 = DC 00

となるわけです。

さて、Java ではどのようにして補助文字をあらわすのでしょうか。JSR-204 を見てみると、当初はいろいろな方法を考えたようです。たとえば、char を 16 bit から 32 bit に変更してしまうという案。言語的にはこれが一番シンプルな方法だと思うのですが、これをすると既存の Java のプログラムとのコンパチビリティが取れません。

Java が出てきたときにはしがらみもなにもなかったので、こんなことは考えなくてもよかったのでしょうが、10 年もたってこれだけメジャーになってしまうとそうもいきません。

ということで結局落ち着いたのが次のような方法です。

  • int を使用して、コードポイントを表す。
    低レベルの API では int であらわしたコードポイントを扱えるようにする。
  • CharSequence は UTF-16 のシーケンスとして扱かう。
    高レベル API では UTF-16 のシーケンスとして文字列をあらわす。
  • char と int であらわしたコードポイントを相互に変換するための API を提供する。

この方針にそって API がアップデートされています。次章ではそのあたりから見ていくことにしましょう。

 

 
 
Tiger はじめはコードポイントを使ってみる
 
 

まずはじめは低レベル API で使用されるコードポイントを使ってみましょう。

見れなくてはしょうがないので、出力することからやってみます。しかし、コードポイントは int なのでたとえば単に System.out.prinln(codePoint); のようにしても数字が出力されてしまいます。

そこで、Formatter クラスを使用してコードポイントを扱うようにします。

サンプルのソース SupplementaryCharTest1.java

直接 Formatter クラスは使用していませんが、format メソッドは裏で Formatter を使用しています。

public class SupplementaryCharTest1 {
    public static void main(String[] args) {
        int codePoint = 0x00010400;
        System.out.format("Char U+0010400 = %c\n", codePoint);
        System.out.format("U+0010400 is %s%n", Character.UnicodeBlock.of(codePoint));
    }
}

通常、コードポイントをあらわすにはなるべく codePoint という名前にしておいたほうがいいと思います。書式の中で %c を使用していますが、%c は今までの char でも int でも扱えるようになっています。char の場合は基本多言語面の文字、int であれば補助文字も含んだコードポイントと解釈されているようです。

Character.UnicodeBlock クラスは Unicode がどのような文字をあらわしているかをあらわすためのクラスです。たとえばひらがなであれば Character.UnicodeBlock.HIRAGANA と定義されています。ある文字がどのようなブロックに属しているかを調べるには Character.UnicodeBlock#of メソッドを使用します。int と char の両方が引数として利用できます。

さて、実行してみましょう。

C:\examples>java SupplementaryCharTest1
Char U+0010400 = ?
U+0010400 is DESERET
 
C:\examples>

U+10400 が DESERET というブロックであることは分かるのですが、肝心の文字が文字化けしてしまっています。これは Windows にこのブロックに対応するフォントがないからです。

表示できないとこまってしまいますね。ところが Tiger ではフォントの扱いも変わっており、jre/lib/fonts/fallback というディレクトリにフォントを置くと、Java2D の描画でここに置かれたフォントを自動的に使用してくれるようになってくれています。

補助文字を含んだフォントですが、James Kass がフリーで提供している Code2001 というフォントがあります。これを使ってみましょう。ダウンロードして、ZIP ファイルを回答すると True Type フォントの CODE2001.TTF というファイルが生成されるので、これを jre/lib/fonts/fallback にコピーします。fallback というディレクトリはデフォルトでは存在しないので、作成して置いてください。

それでは、先ほどのプログラムを Java2D を使用するように変更してみます。Java2D というとどのように変更すればよく分からないかもしれませんが、単に AWT や Swing で文字を表示させればいいだけです。

サンプルのソース SupplementaryCharTest1_1.java

文字の描画には JTextArea クラスを使用しています。

import javax.swing.JFrame;
import javax.swing.JTextArea;
 
public class SupplementaryCharTest1_1 {
    public static void main(String[] args) {
        int codePoint = 0x00010400;
 
        JFrame frame = new JFrame("SupplementChar");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(200, 200);
 
        JTextArea area = new JTextArea();
        area.append(String.format("Char U+0010400 = %c\n", codePoint));
        area.append(String.format("U+0010400 is %s%n", 
                                  Character.UnicodeBlock.of(codePoint)));
 
        frame.getContentPane().add(area);
        frame.setVisible(true);
    }
}

先ほどは PrintStream#format メソッドを使用しましたが、文字列を作成するので String#format メソッドを使用しました。

これを実行すると、次のようなフレームが表示されます。

SupplementaryCharTest1_1 の結果
図 1 SupplementaryCharTest1_1 の結果

ところで U+10400 があらわしている偏微分のマークのような文字は DESERET であらわされているように、Deseret Alphabet と呼ばれているものらしいです。これはユタ州の Deseret University (現 Utah University) で考案されたものだそうです。

 

 
 
Tiger コードポイントから UTF-16 へ
 
 

int 型でコードポイントはあらわせることは分かりましたが、これを UTF-16 に変換できなければ使い物になりません。

サンプルのソース SupplementaryCharTest2.java

1 文字単位の変換には Character#toChars メソッドを使用します。

        int codePoint = 0x00010400;
        char[] ch = Character.toChars(codePoint);
 
        System.out.format("ch[0] = %X ch[1] %X\n", (int)ch[0], (int)ch[1]);

char の配列になってしまうと、どこが上位サロゲートでどこが下位サロゲートか分かりにくくなってしまいます。そこで、上位サロゲートか下位サロゲートかを調べるためのメソッドも用意されています。

        System.out.format("%X is High: %b Low: %b%n",
                          (int)ch[0], 
                          Character.isHighSurrogate(ch[0]),
                          Character.isLowSurrogate(ch[0]));
        System.out.format("%X is High: %b Low: %b%n", 
                          (int)ch[1],
                          Character.isHighSurrogate(ch[1]),
                          Character.isLowSurrogate(ch[1]));

Character#isHighSurrogate メソッドが上位サロゲートかどうかを調べるメソッド、Character#isLowSurrogate メソッドが下位サロゲートかどうかを調べるメソッドです。

1 文字ではなくて、複数の文字をコードポイントで表している場合は String クラスを使用します。String クラスのコンストラクタに int の配列を取るものが追加されたので、それを使用します。

        String str = new String(new int[]{codePoint}, 0, 1);
        JOptionPane.showMessageDialog((JFrame)null, 
                                      String.format("U+0010400 : %s%n", str));

実行すると、最後のダイアログはコマンドプロンプトには以下のように出力されました。

C:\examples>java SupplementaryCharTest2
ch[0] = D801 ch[1] DC00
D801 is High: true Low: false
DC00 is High: false Low: true
 
C:\examples>

当たり前ですが、ただしく U+10400 が D801 と DC00 に分解されているのが確認できます。

上位サロゲートか下位サロゲートかは自分で調べるのも簡単なのですが、提供されているものを使ったほうがプログラムが見やすくなりますね。

 

 
 
Tiger char からコードポイントへ
 
 

今度は逆に char からコードポイントへ変換してみましょう。

サンプルのソース SupplementaryCharTest3.java

コードポイントへの変換には Character#toCodePoint メソッドを使用します。

        char[] ch = new char[]{(char)0xD801, (char)0xDC00};
        
        int codePoint = Character.toCodePoint(ch[0], ch[1]);
        System.out.format("UTF-16 %X %X -> %X\n",
                          (int)ch[0], (int)ch[1], codePoint);

toCodePoint メソッドの引数は char が 2 つになります。基本多言語面の文字の場合は単に上位サロゲートを 0 にすればいいようです。

注意が必要なのは、このメソッドは単に数値計算をしているだけで、その結果が正しいコードポイントになっているかどうかはチェックしていないということです。

もし、このチェックが必要な場合は Character#isSurrogatePair メソッドを使用します。

複数文字を int の配列にするためのメソッドは用意されていないようです。もしかすると見逃しているかもしれません。もし、見逃しているようでしたら櫻庭までメールしてください。

さて、String クラスはコードポイントを使用した int の配列でも、UTF-16 のシーケンスとしての char の配列でも生成することができます。この 2 つの方法で生成された文字列は果たして同じ内容を保持しているのでしょうか。

        String str1 = new String(new int[]{codePoint}, 0, 1);
        String str2 = new String(ch);
        System.out.format("U+0010400 == UTF-16 D801 DC00 : %b%n",
                          str1.equals(str2));

これを実行してみると true が出力されるので、同じなのでしょう。

C:\examples>java SupplementaryCharTest3
UTF-16 D801 DC00 -> 10400
U+0010400 == UTF-16 D801 DC00 : true
 
C:\examples>

実際に String クラスでは内部でどのように文字を保持しているのでしょうか。String クラスは高レベルな API になるので UTF-16 シーケンスを保持しているはずです。

String.java を見てみると確かに UTF-16 で保持しているようです。たとえば、char[] の引数をとるコンストラクタを見てみると、

    public String(char value[]) {
        int size = value.length;

        char[] v = new char[size];

        System.arraycopy(value, 0, v, 0, size);

        this.offset = 0;

        this.count = size;

        this.value = v;
    }

プロパティ value で UTF-16 シーケンスを持っているようです。

複数の文字を int の配列に変換することはできないようですが、文字列の任意の文字をコードポイントに変換することはできます。String#codePointAt メソッドもしくは Character#codePointAt メソッドを使用します。

また、1 文字前をコードポイントにする String#codePointBefore メソッド、Character#codePointBefore メソッドというのもあります。

サンプルのソース SupplementaryCharTest4.java

String#codePointAt メソッドの引数はインデックスですが、これは char の配列のインデックスなのでしょうか、それとも文字のインデックスなのでしょうか、試してみましょう。

public class SupplementaryCharTest4 {
    public static void main(String[] args) {
        char[] ch = new char[]{(char)0xD801, (char)0xDC00,
                               (char)0xD801, (char)0xDC01,
                               (char)0xD801, (char)0xDC02,
                               (char)0xD801, (char)0xDC03,
                               (char)0xD801, (char)0xDC04,
                               (char)0xD801, (char)0xDC05,
                               (char)0xD801, (char)0xDC06,
                               (char)0xD801, (char)0xDC07,
                               (char)0xD801, (char)0xDC08,
                               (char)0xD801, (char)0xDC09,
                               (char)0xD801, (char)0xDC0A};
        String str = new String(ch);
 
        int codePoint1 = str.codePointAt(0);
        System.out.format("CodePoint at 0 : %X%n", codePoint1);
        int codePoint2 = str.codePointAt(1);
        System.out.format("CodePoint at 1 : %X%n", codePoint2);
        int codePoint3 = str.codePointAt(2);
        System.out.format("CodePoint at 2 : %X%n", codePoint3);
        int codePoint4 = str.codePointAt(3);
        System.out.format("CodePoint at 3 : %X%n", codePoint4);
 
        int codePoint5 = str.codePointBefore(2);
        System.out.format("CodePoint before 2 : %X%n", codePoint5);
        int codePoint6 = str.codePointBefore(3);
        System.out.format("CodePoint before 3 : %X%n", codePoint6);
        int codePoint7 = str.codePointBefore(4);
        System.out.format("CodePoint before 4 : %X%n", codePoint7);
        int codePoint8 = str.codePointBefore(5);
        System.out.format("CodePoint before 5 : %X%n", codePoint8);
    }
}

これを実行してみました。

C:\examples>java SupplementaryCharTest4
CodePoint at 0 : 10400
CodePoint at 1 : DC00
CodePoint at 2 : 10401
CodePoint at 3 : DC01
CodePoint before 2 : 10400
CodePoint before 3 : D801
CodePoint before 4 : 10401
CodePoint before 5 : D801
 
C:\examples>

どうやら、char 配列のインデックスのようです。また、ここから分かることとして codePointAt メソッドはインデックスが上位サロゲートの位置であれば正しくコードポイントに変換してくれるのですが、下位サロゲートの場合はそのままそのインデックスの値になってしまうということです。

同様に CodePointBefore メソッドもインデックスが上位サロゲートであればいいのですが、下位サロゲートの場合は 1 つ前の下位サロゲートを戻してくるということです。

厳密に行うのであれば、インデックスが示している文字が上位サロゲートかどうかを調べなくてはいけないようです。

ここでふとした疑問が。今まで文字列の長さは char 配列の長さだったのですが、補助文字をサポートしたら長さはどうなっているのでしょうか。これも試してみましょう。

サンプルのソース SupplementaryCharTest5.java

 

public class SupplementaryCharTest5 {
    public static void main(String[] args) {
        char[] ch = new char[]{(char)0xD801, (char)0xDC00,
                               (char)0xD801, (char)0xDC01,
                               (char)0xD801, (char)0xDC02,
                               (char)0xD801, (char)0xDC03,
                               (char)0xD801, (char)0xDC04,
                               (char)0xD801, (char)0xDC05,
                               (char)0xD801, (char)0xDC06,
                               (char)0xD801, (char)0xDC07,
                               (char)0xD801, (char)0xDC08,
                               (char)0xD801, (char)0xDC09,
                               (char)0xD801, (char)0xDC0A};
        String str = new String(ch);
 
        System.out.format("Length of String : %d%n", str.length());
    }
}

実行すると...

C:\examples>java SupplementaryCharTest5
Length of String : 22
 
C:\examples>

ありゃりゃ、配列の長さになっています。これでは本当の文字数がわかりません。困った。

よく探してみたら、codePointCount というメソッドがありました。これを使うようです。

サンプルのソース SupplementaryCharTest5_1.java

 

public class SupplementaryCharTest5_1 {
    public static void main(String[] args) {
        char[] ch = new char[]{(char)0xD801, (char)0xDC00,
                               (char)0xD801, (char)0xDC01,
                               (char)0xD801, (char)0xDC02,
                               (char)0xD801, (char)0xDC03,
                               (char)0xD801, (char)0xDC04,
                               (char)0xD801, (char)0xDC05,
                               (char)0xD801, (char)0xDC06,
                               (char)0xD801, (char)0xDC07,
                               (char)0xD801, (char)0xDC08,
                               (char)0xD801, (char)0xDC09,
                               (char)0xD801, (char)0xDC0A};
        String str = new String(ch);
 
        System.out.format("Length of String : %d%n", str.length());
        System.out.format("CodePointCount of String : %d%n",
                          str.codePointCount(0, str.length()));
    }
}

この場合は正しく 11 となりました。

C:\examples>java SupplementaryCharTest5_1
Length of String : 22
CodePointCount of String : 11
 
C:\examples>

補助文字とそれ以外の文字がまざっていたらどうなるのでしょうか。これも試してみましょう。

サンプルのソース SupplementaryCharTest5_2.java

 

public class SupplementaryCharTest5_2 {
    public static void main(String[] args) {
        char[] ch = new char[]{(char)0x20,
                               (char)0x21,
                               (char)0x22,
                               (char)0xD801, (char)0xDC00,
                               (char)0xD801, (char)0xDC01,
                               (char)0x23,
                               (char)0xD801, (char)0xDC02,
                               (char)0xD801, (char)0xDC03,
                               (char)0xD801, (char)0xDC04,
                               (char)0x24};
 
        String str = new String(ch);
 
        System.out.format("Length of String : %d%n", str.length());
        System.out.format("CodePointCount of String : %d%n",
                          str.codePointCount(0, str.length()));
    }
}

さて、ちゃんと文字数が出力できるでしょうか?

C:\examples>java SupplementaryCharTest5_2
Length of String : 15
CodePointCount of String : 10
 
C:\examples>

正しく文字数が出力されました。

ということは、今後は文字数を調べるときは length メソッドではなく codePointCount メソッドを使わなくてはいけないということになります。

これはかなり重要な Idiom になりそうです。

困ったことに同じようなことは substring などにもあてはまります。substring などの引数はコードポイントを認識してくれず、いままでと同じように char の配列のインデックスとなってしまいます。どうにかならないものでしょうか。

いちいちサロゲートかどうかを調べなくてはいけないのは結構面倒です。

 

 
 
Tiger StringBuffer/StringBuilder の場合
 
 

今までは String クラスで試してきましたが、StringBuffer/StringBuilder クラスではどうでしょうか。

一番の問題はコードポイントをアペンドするときです。コードポイントは int なのでそのまま append メソッドを使用すると、単に数字として認識されてしまいます。

このため、appendCodePoint メソッドが新たに定義されました。

しかし、挿入の方はコードポイントを扱うメソッドはないようです。しかたないので、一度 char の配列に直すか、String クラスなどの CarSequence インタフェースの実装クラスを使用するしかなさそうです。

 

 
 
Tiger その他のクラスなど
 
 

補助文字をサポートすることによって変更があったクラスがいくつかあります。

新規のクラスとしては java.util.Formatter クラスがあります。int を %c で出力すると、コードポイントとして認識されます。これはすでに前述したサンプルの SupplementaryCharTest1 クラスで使っています。

そのほかには正規表現が補助文字に対応されています。java.util.regex.Pattern/Matcher クラスだけでなく、内部的にこれらのクラスを使用しているクラスでも同じです。かといって、特に使い方に違いはありません。

注意しなくてはならないのが、プロパティファイルなどです。プロパティファイルは日本語などをユニコードシーケンス (\uXXXX であらわされるものです) を使ってあらわします。ここで扱えるのは UTF-16 です。UTF-32 は使用できません。

もちろん、プロパティファイルを XML で書く場合は文字コードを指定できるので、任意の文字コードを使用して記述することができます。

 

 
 
Tiger おわりに
 
 

補助文字なんて関係ないと思っていませんか。実は一部の漢字も補助文字に入っています。CJK Unified Ideographs Extension などがそれに相当します。他人事ではないということなんです。

補助文字がサポートされて、アプリケーションは影響をうけるのでしょうか。Java プラットフォームにおける補助文字のサポート というドキュメントにはアプリケーションを 3 つのタイプに分けています。

  1. CharSequence インタフェースだけを使用して文字列を扱うアプリケーション
  2. 個々の文字を独自に扱う必要があるアプリケーション
  3. 個々の文字を独自に扱う必要があるアプリケーションのうち、補助文字を扱う可能性のあるアプリケーション

1 のタイプのアプリケーションでは一般的に変更する必要はありません。

2 のタイプのアプリケーションでは個々の文字が有効かどうか調べる必要があります。ただし、変更する可能性はあまりありません。

問題は 3 のタイプのアプリケーションです。このタイプのアプリケーションは個々の文字を調べるのは当たり前ですが、補助文字の場合は int のコードポイントに変換して扱う必要があるかもしれません。

でも、コードポイントと UTF-16 を混ぜて使うのはいまいちすっきりしません。やっぱり仕様的にすっきりするのは char を 32 bit にすることだと思うのですが、これはいろいろと問題があるのでしかたないのでしょう。

 

今回使用したサンプルはここからダウンロードできます。

 

参考

 

(Jun. 2004)

 
 
Go to Contents Go to Java Page