Unicodeの基礎知識とJavaでの扱い

前置き

Unicodeといえば、Shift_JISやEUC-JPと並ぶ文字コードの一種です。では、UCS-2やUTF-8は何なのでしょう? Unicodeとは似て非なるもの? Unicodeの別名? その辺の違いを整理しつつ、Javaプログラム内でUnicode文字列を扱うための基礎知識をまとめてみます。

.NET Frameworkでの文字コードの扱いについては、こちらの記事を参照して下さい。

本文

文字コード

「文字コード」という言葉には、いろんな意味があります。語彙の定義は一つかもしれませんが、実際にはいろんな意味を持って使われるのです。なので厳密な定義を覚えようとせず、結局は文脈で判断する方が良いと思います。ここでは、「文字コード」が意味しうるものを3つ挙げておきます。

文字と数値の対応関係
ある文字集合(文字セット)を定め、その各文字に一意な数値を対応付けた体系のことを「文字コード」と呼ぶ場合があります。例えばShift_JISは文字コードの一種であり、「文」を0x95B6に、「字」を0x8E9Aに対応付けています。同じ「文」でも、JISなら0x4A38だし、EUC-JPなら0xCAB8に対応付けられています。
文字に対応付けられた数値そのもの
ある文字に対応付けられた数値そのもののことを「文字コード」と呼ぶ場合があります。例えばShift_JISなら、「文」の文字コードは0x95B6です。
符号化方式
文字(あるいは文字列)を、8bit(=1バイト)データの羅列に変換する方法のことを「文字コード」と呼ぶ場合があります。例えばWindowsのテキストファイルで一般的に使われる文字コードはShift_JISです。

ちなみに、テキストファイルに「文字」と書いてShift_JISで保存すると、そのファイルのサイズは4バイトで、先頭から順に0x95、0xB6、0x8E、0x9Aが書かれているはずです。これは当たり前に見えるかもしれませんが、見掛けほど単純な話しではありません。例えばエンディアンが気になりませんか? リトルエンディアンに慣れたプログラマーなら、「文(=0x95B6)」は、0xB6、0x95の順に保存される方が違和感が少ないでしょう。しかし実際はシステムがどっちのエンディアンを採用してようと、Shift_JISの文字はビッグエンディアン的に符号化されます。また上記の例では1文字のサイズが2バイトでしたが、ASCII文字なら1バイトです(Shift_JISの場合)。符号化方式には、サイズの異なる文字が混在した場合でも、あとから正しく解釈できる(文字と文字の境界が分からなくなったりしない)ことが求められます。

本記事の「文字コード」

本記事での「文字コード」の意味は、「ある文字集合における文字と数値とのマッピング体系」です。つまり、符号化方式とは区別して使います。

Unicodeとコードポイント

Unicodeとは文字コードの一種です。1つの文字を21bitの数値で表現しますが、Unicodeではこの数値のことをコードポイントと呼びます。例えば'a'という文字のコードポイントは、16進数で書けば、0x000061です。「漢」という文字なら、0x006F22です。また「正しい「叱」」という文字なら、0x020B9Fです。

16進数の数値を表記するとき、一般には 0x に続けて数値を書きますが、コードポイントを表記するときは、U+ に続けて数値を書きます。21bitなので6桁になりますが、最初の2桁が0の場合は省略して4桁や5桁で表記するのが普通です。よって、前述の3文字の場合、それぞれ、U+0061、U+6F22、U+20B9Fとなります。

ちなみに「正しい「叱」」は、口に七と書いています。一般に良く使われる、口にヒと書いた「叱」の方(U+53F1)とは別の文字です。

21bitで表現できる数値の範囲(unsignedの場合)は0x000000~0x1FFFFFですが、UnicodeではU+0000~U+10FFFFを使います。上位の5bitと下位の16bitに分けて考えると、上位の5bitは0x00~0x10(10進数なら0~16)になります。この上位5bitの部分をと呼び、21bit値の空間を第0面から第16面の計17面に分けています。

第0面はBMP(Basic Multilingual Plane、基本多言語面)と呼ばれ、ほとんどの常用文字がこの面のコードポイントに割り振られています。後で出てきますが、BMPのコードポイントは4桁の16進数で表記できるという点も重要です。またASCIIコードの文字は、UnicodeでもU+0000~U+007Fに割り当てられており、下位7ビット分だけ見れば互換性があります。

日本語であれば、BMP以外にも、第2面のSIP(Supplementary Ideographic Plane、追加漢字面)が使われます。また第15面と第16面はPUA(Private Use Area、私用領域、私用面)と呼ばれ、文字とコードポイントとのマッピングをローカルに決めることができる領域です。私用が可能な領域はBMPの一部(U+E000~U+F8FF)にもあります。私用領域は、携帯電話の絵文字などに使われているようです。

符号化方式(エンコーディング)

21bitのコードポイントをプログラム内で扱う場合、単純に考えると1文字を32bitデータ(C言語ならunsigned long値とか)で表現するのが素直に見えます。しかし、多くの文字がBMP(つまり16bitで表現できる領域)に収まっていることを考えると、これは非効率です。また、ASCII文字しか使わない場合は16bitでも非効率でしょう。そこで、Unicodeには何種類かの符号化方式が用意されています。

UTF-8

UTF(Unicode Transformation Format)-8は、コードポイントを1~4バイトに符号化する方式です。符号化後のデータ長はコードポイントに応じて変わります。中でも、U+0000~U+007Fの文字は1バイトデータとして符号化されるので、ASCIIコードと互換性があります。

具体的な符号化方法は以下の通りです。

  • U+0000~U+007Fは、有効bit数が7で、それをそのまま使って1バイトとする
  • U+0080~U+07FFは、有効bit数の11を上位5bitと下位6bitに分け、それぞれに0xC0と0x80を足して2バイトとする
  • U+0800~U+FFFFは、有効bit数16を上位から4bit、6bit、6bitに分け、それぞれに0xE0、0x80、0x80を足して3バイトとする
  • U+10000~U+10FFFFは、有効bit数21を上位から3bit、6bit、6bit、6bitに分け、それぞれに0xF0、0x80、0x80、0x80を足して4バイトとする

UTF-8では、ビッグエンディアンっぽいバイトオーダーを使います。つまり上位のバイトが先に来ます。例えば「漢(U+6F22)」を上述のルールに当てはめると、4bit、6bit、6bitに分け(0x06、0x3C、0x22)て、それぞれに0xE0、0x80、0x80を足した、0xE6、0xBC、0xA2が、この順序で並びます。

UTF-16、サロゲートペア、BOM

UTF-16は、コードポイントを1つか2つの16bit値に符号化する方式です。BMP内のコードポイント(U+000000~U+00FFFF)は、下位16bitをそのまま使って2バイトに符号化します。この2バイトデータを符号単位(code unit)と呼んだりします。一方、BMP以外の面上のコードポイント(U+010000~U+10FFFF)は、2つの符号単位を使って符号化します。この2つの2バイトデータのことをサロゲートペア(surrogate pair、代用対)と呼びます。

サロゲートペアについてもう少し詳しく説明しましょう。実はBMPには未使用の領域があり、このコードポイントには文字を割り当てないことになっています。これがU+D800~U+DBFFの1024個と、U+DC00~U+DFFFの1024個です。この未使用領域のコードポイントを2つ組み合わせれば、1048576(=1024*1024)文字を表現できます。つまりBMP以外の面上の全文字数16*256*256(=1048576)とピッタリ一致します。

例えばU+10000なら、U+D800とU+DC00の2つのコードポイント、つまり、0xD800と0xDC00の2つの16bit値(=符号単位)に符号化されます。最初の符号単位が上位サロゲート、あとの方が下位サロゲートです。同様に、U+10001ならU+D800とU+DC01、U+10002ならU+D800とU+DC02、といった具合に続きます。

UTF-16のバイトオーダーは自由です。例えば「漢(U+6F22)」なら、ビッグエンディアンで0x6F、0x22としても、リトルエンディアンで0x22、0x6Fとしても構いません。そこで、読み手にバイトオーダーを伝える手段が2種類用意されています。

1つは、符号化方式の名前で区別する方法で、ビッグエンディアンならUTF-16BE、リトルエンディアンならUTF-16LEと呼び分けます。もう一つは、データの先頭にBOM(Byte Order Mark)という特殊文字U+FEFFを付ける方法です。BOM方式の場合、先頭が0xFE、0xFFとなっていればビッグエンディアン、0xFF、0xFEとなっていればリトルエンディアンと判別できます。

なおバイトオーダーに関係なく、サロゲートペアの文字は、上位サロゲートが先に来ます。

余談ですが、UTF-8にBOMは付けません。無理に付けるとすれば、U+FEFFをUTF-8で符号化した0xEF、0xBB、0xBFの3バイトが付きますが、これは国際的には通用しないbad practiceみたいです(これを実践しているアプリも世の中にはあるようですが…)。

UTF-32

UTF-32は、全てのコードポイントをそのまま32bit値に符号化する方式です。

UTF-16のようなサロゲートペアは不要です。

バイトオーダーに関してはUTF-16と同様で、UTF-32BEやUTF-32LEのように呼び分ける方法や、BOM(0x00、0x00、0xFE、0xFFならビッグエンディアン、0xFF、0xFE、0x00、0x00ならリトルエンディアン)で判別する方法が使えます。

UCS

UCS(Universal Multiple-Octet Coded Character Set)は、Unicodeと似たような文字コードの一種です。正式名はISO/IEC 10646なので、ISO規格ということですね。1文字を32bitで表現するのですが、「Unicodeで使えない領域(0x110000以上)には文字を割り当てない」という決まりらしいので、実質的にはUnicodeと同じかもしれません。何となく、アメリカ対ヨーロッパの規格争いの匂いがします。

Unicodeと同様に、よく使う文字は16bit領域内に収められておりBMPと呼ばれます。UCSのBMPにはUCS-2という別名も用意されています。BMP内の文字のみからなる文字セットを表現する場合にUCS-2と呼ぶことがあるようです。

UCS-2は符号化方式の名称でもあります。UTF-16と同様に1文字を2バイトで表現しますが、BMPの文字を符号化できれば十分なのでサロゲートペアのような仕組みは持ちません。一方、UCSの全文字を符号化できる符号化方式としては、UCS-4が用意されています。UCS-4はUTF-32と同じと考えて良さそうです。

UCSの符号化にはUTF-8やUTF-16も使えることになっています。しかし、この場合のUTFはUCS Transformation Formatの略だそうです。やれやれ。

JavaでUnicodeを扱う

Java内部での文字表現はUTF-16です。つまりCharacterオブジェクトやStringオブジェクトの内部では、文字データがUTF-16で保持されています。char値がUTF-16の符号単位に相当し、サロゲートペアも適用されます。バイトオーダーは(たぶん)ビッグエンディアンで、BOMは付きません。コードポイントの表現にはint値が使われます。CharacterクラスやStringクラスには、Unicode文字の属性を調べるためのメソッドが多数用意されています。以下に、例を挙げます。

Character#charCount()
任意のコードポイントが何個の符号単位になるか(1か2か)を返す。
Character#isLowSurrogate()
任意のchar値が下位サロゲートかどうかを返す。
String#codePointCount()
文字列長を、char数ではなくコードポイント数で返す。
String#offsetByCodePoint()
文字列のインデックスを、char数ではなくコードポイント数で変化させる。
String#getBytes()
文字列を任意の符号化方式(Shift_JISとかUTF-8とか)でエンコードしてbyte[]に変換する。
String#String()
任意の方式でエンコードされたbyte[]からStringオブジェクトを生成する。

普通ならStringオブジェクトを切ったり貼ったりして加工するのは楽チンなのですが、サロゲートペアが絡むと「char値1つが1文字」という想定が崩れるので、途端に面倒くさくなります。例えば、Stringオブジェクトから部分文字列を切り出すときは、サロゲートペアを分断しないように考慮しなければなりません。この辺のpracticeに関して、IBM DeveloperWorksのサイトにUnicode surrogate programming with the Java languageという、すばらしい記事があるので参考にして下さい(日本語訳もあります)。

また、Stringオブジェクトと、エンコードされたbyte[]との相互変換には可逆性が無い場合があります。例えばString#getBytes("Shift_JIS")では、サロゲートペアの文字が'?'に化けると思います。このbyte[]をStringコンストラクタに渡すと、'?'のコードポイントU+003FとしてStringオブジェクトが生成されます。サロゲートペアの文字以外にも、エンコード先の文字コードでサポートされてない文字に対しては、このような現象が起きると思います。

Last modified:2011/05/10 14:26:37
Keyword(s):
References:[言語Tips] [VC.NET ~ 文字コード変換]
This page is frozen.