|
Unicode 4.0 の補助文字のサポート Supplementary 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 年もたってこれだけメジャーになってしまうとそうもいきません。 ということで結局落ち着いたのが次のような方法です。
この方針にそって API がアップデートされています。次章ではそのあたりから見ていくことにしましょう。
|
|
|||||||||||
まずはじめは低レベル API で使用されるコードポイントを使ってみましょう。 見れなくてはしょうがないので、出力することからやってみます。しかし、コードポイントは int なのでたとえば単に System.out.prinln(codePoint); のようにしても数字が出力されてしまいます。 そこで、Formatter クラスを使用してコードポイントを扱うようにします。
直接 Formatter クラスは使用していませんが、format メソッドは裏で Formatter を使用しています。
通常、コードポイントをあらわすにはなるべく codePoint という名前にしておいたほうがいいと思います。書式の中で %c を使用していますが、%c は今までの char でも int でも扱えるようになっています。char の場合は基本多言語面の文字、int であれば補助文字も含んだコードポイントと解釈されているようです。 Character.UnicodeBlock クラスは Unicode がどのような文字をあらわしているかをあらわすためのクラスです。たとえばひらがなであれば Character.UnicodeBlock.HIRAGANA と定義されています。ある文字がどのようなブロックに属しているかを調べるには Character.UnicodeBlock#of メソッドを使用します。int と char の両方が引数として利用できます。 さて、実行してみましょう。
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 で文字を表示させればいいだけです。
文字の描画には JTextArea クラスを使用しています。
先ほどは PrintStream#format メソッドを使用しましたが、文字列を作成するので String#format メソッドを使用しました。 これを実行すると、次のようなフレームが表示されます。
ところで U+10400 があらわしている偏微分のマークのような文字は DESERET であらわされているように、Deseret Alphabet と呼ばれているものらしいです。これはユタ州の Deseret University (現 Utah University) で考案されたものだそうです。
|
|
||||||||
int 型でコードポイントはあらわせることは分かりましたが、これを UTF-16 に変換できなければ使い物になりません。
1 文字単位の変換には Character#toChars メソッドを使用します。
char の配列になってしまうと、どこが上位サロゲートでどこが下位サロゲートか分かりにくくなってしまいます。そこで、上位サロゲートか下位サロゲートかを調べるためのメソッドも用意されています。
Character#isHighSurrogate メソッドが上位サロゲートかどうかを調べるメソッド、Character#isLowSurrogate メソッドが下位サロゲートかどうかを調べるメソッドです。 1 文字ではなくて、複数の文字をコードポイントで表している場合は String クラスを使用します。String クラスのコンストラクタに int の配列を取るものが追加されたので、それを使用します。
実行すると、最後のダイアログはコマンドプロンプトには以下のように出力されました。
当たり前ですが、ただしく U+10400 が D801 と DC00 に分解されているのが確認できます。 上位サロゲートか下位サロゲートかは自分で調べるのも簡単なのですが、提供されているものを使ったほうがプログラムが見やすくなりますね。
|
|
||||||||||||||||||||||||
今度は逆に char からコードポイントへ変換してみましょう。
コードポイントへの変換には Character#toCodePoint メソッドを使用します。
toCodePoint メソッドの引数は char が 2 つになります。基本多言語面の文字の場合は単に上位サロゲートを 0 にすればいいようです。 注意が必要なのは、このメソッドは単に数値計算をしているだけで、その結果が正しいコードポイントになっているかどうかはチェックしていないということです。 もし、このチェックが必要な場合は Character#isSurrogatePair メソッドを使用します。 複数文字を int の配列にするためのメソッドは用意されていないようです。もしかすると見逃しているかもしれません。もし、見逃しているようでしたら櫻庭までメールしてください。 さて、String クラスはコードポイントを使用した int の配列でも、UTF-16 のシーケンスとしての char の配列でも生成することができます。この 2 つの方法で生成された文字列は果たして同じ内容を保持しているのでしょうか。
これを実行してみると true が出力されるので、同じなのでしょう。
実際に String クラスでは内部でどのように文字を保持しているのでしょうか。String クラスは高レベルな API になるので UTF-16 シーケンスを保持しているはずです。 String.java を見てみると確かに UTF-16 で保持しているようです。たとえば、char[] の引数をとるコンストラクタを見てみると、
プロパティ value で UTF-16 シーケンスを持っているようです。 複数の文字を int の配列に変換することはできないようですが、文字列の任意の文字をコードポイントに変換することはできます。String#codePointAt メソッドもしくは Character#codePointAt メソッドを使用します。 また、1 文字前をコードポイントにする String#codePointBefore メソッド、Character#codePointBefore メソッドというのもあります。
String#codePointAt メソッドの引数はインデックスですが、これは char の配列のインデックスなのでしょうか、それとも文字のインデックスなのでしょうか、試してみましょう。
これを実行してみました。
どうやら、char 配列のインデックスのようです。また、ここから分かることとして codePointAt メソッドはインデックスが上位サロゲートの位置であれば正しくコードポイントに変換してくれるのですが、下位サロゲートの場合はそのままそのインデックスの値になってしまうということです。 同様に CodePointBefore メソッドもインデックスが上位サロゲートであればいいのですが、下位サロゲートの場合は 1 つ前の下位サロゲートを戻してくるということです。 厳密に行うのであれば、インデックスが示している文字が上位サロゲートかどうかを調べなくてはいけないようです。 ここでふとした疑問が。今まで文字列の長さは char 配列の長さだったのですが、補助文字をサポートしたら長さはどうなっているのでしょうか。これも試してみましょう。
実行すると...
ありゃりゃ、配列の長さになっています。これでは本当の文字数がわかりません。困った。 よく探してみたら、codePointCount というメソッドがありました。これを使うようです。
この場合は正しく 11 となりました。
補助文字とそれ以外の文字がまざっていたらどうなるのでしょうか。これも試してみましょう。
さて、ちゃんと文字数が出力できるでしょうか?
正しく文字数が出力されました。 ということは、今後は文字数を調べるときは length メソッドではなく codePointCount メソッドを使わなくてはいけないということになります。 これはかなり重要な Idiom になりそうです。 困ったことに同じようなことは substring などにもあてはまります。substring などの引数はコードポイントを認識してくれず、いままでと同じように char の配列のインデックスとなってしまいます。どうにかならないものでしょうか。 いちいちサロゲートかどうかを調べなくてはいけないのは結構面倒です。
|
|
||||
今までは String クラスで試してきましたが、StringBuffer/StringBuilder クラスではどうでしょうか。 一番の問題はコードポイントをアペンドするときです。コードポイントは int なのでそのまま append メソッドを使用すると、単に数字として認識されてしまいます。 このため、appendCodePoint メソッドが新たに定義されました。 しかし、挿入の方はコードポイントを扱うメソッドはないようです。しかたないので、一度 char の配列に直すか、String クラスなどの CarSequence インタフェースの実装クラスを使用するしかなさそうです。
|
|
||||
補助文字をサポートすることによって変更があったクラスがいくつかあります。 新規のクラスとしては java.util.Formatter クラスがあります。int を %c で出力すると、コードポイントとして認識されます。これはすでに前述したサンプルの SupplementaryCharTest1 クラスで使っています。 そのほかには正規表現が補助文字に対応されています。java.util.regex.Pattern/Matcher クラスだけでなく、内部的にこれらのクラスを使用しているクラスでも同じです。かといって、特に使い方に違いはありません。 注意しなくてはならないのが、プロパティファイルなどです。プロパティファイルは日本語などをユニコードシーケンス (\uXXXX であらわされるものです) を使ってあらわします。ここで扱えるのは UTF-16 です。UTF-32 は使用できません。 もちろん、プロパティファイルを XML で書く場合は文字コードを指定できるので、任意の文字コードを使用して記述することができます。
|
|
||||
補助文字なんて関係ないと思っていませんか。実は一部の漢字も補助文字に入っています。CJK Unified Ideographs Extension などがそれに相当します。他人事ではないということなんです。 補助文字がサポートされて、アプリケーションは影響をうけるのでしょうか。Java プラットフォームにおける補助文字のサポート というドキュメントにはアプリケーションを 3 つのタイプに分けています。
1 のタイプのアプリケーションでは一般的に変更する必要はありません。 2 のタイプのアプリケーションでは個々の文字が有効かどうか調べる必要があります。ただし、変更する可能性はあまりありません。 問題は 3 のタイプのアプリケーションです。このタイプのアプリケーションは個々の文字を調べるのは当たり前ですが、補助文字の場合は int のコードポイントに変換して扱う必要があるかもしれません。 でも、コードポイントと UTF-16 を混ぜて使うのはいまいちすっきりしません。やっぱり仕様的にすっきりするのは char を 32 bit にすることだと思うのですが、これはいろいろと問題があるのでしかたないのでしょう。
今回使用したサンプルはここからダウンロードできます。
参考
(Jun. 2004) |
|