国際化プログラミング(の前の知識)
書籍「パーフェクトJava」に掲載予定だった「国際化」の章の原稿(前半)です。ページ数の関係で掲載していません。Javaプログラミングと直接的な関係は薄いですが、国際化プログラミングの背景として知っておいて損はありません。 掲載予定だった「国際化」の章の後半: http://dev.ariel-networks.com/column/tech/i18n-part2/ パーフェクトJava http://www.amazon.co.jp/dp/4774139904/
■■■18-1 国際化と地域化
国際化とはinternationalizationの訳語です。長い単語なので、先頭のiと末尾のnの間に18文字あることからしばしばi18nと省略します。地域化はlocalizationの訳語です。同様の省略でl10nと表記します。本書では国際化と地域化の記述で統一します。
普通にプログラムを書いている時、出力テキスト(メッセージ)の英語をそのままソースコード内に書くと、そのプログラムは英語の出力しかできません。ソースコードを書き換えてメッセージを日本語にすると、今度は日本語しか出力できなくなります。英語と日本語の両方を出力するためには条件分岐のコードを書き足す必要があります。もし中国語の対応が必要になった時はメッセージのある箇所の条件分岐のすべてを変更して再コンパイルしなければいけません。遊びのプログラムなら許せますが、実用プログラムでこれは現実的ではありません。同様の問題はメッセージ以外にも存在します。
このような課題に対してプログラミングの世界では、ソフトウェアを国際化することで対処します。国際化の基本的な発想は、言語や国に依存する部分を分離してソースコードの外に追い出すことです。出力メッセージの話を続けると、出力メッセージ用の文字列をソースコードにハードコードせずに外部に出します。追い出した言語ごとのファイルを出力メッセージファイルと呼びます。こうして実行時に必要な言語の出力メッセージファイルを読むようにします。
言語や国や文化のような地域依存性を総称して「ロケール(locale)」と呼びます。ソフトウェアの国際化とは、プログラムからロケール依存の情報を外部に追い出し、実行時にロケール依存の情報を読み出せるようにすることです。ロケールについては節を改めて説明します。
国際化をしても、実際に日本語のメッセージや中国語のメッセージを用意(翻訳作業)する作業は残ります。国際化したプログラムに対して、ロケール依存の情報を用意することを地域化と呼びます。多くの場合、地域化は、英語メッセージから日本語などの別の言語への翻訳を意味します。
紛らわしいことに、地域化という用語を、ソースコードの中に直接日本語をハードコードしてプログラムを日本語対応する作業に使うこともあります。言わば場当たり的な地域化です。国際化よりも安易で、ある局面ではこれで充分な場合もあるため、今でもこのような地域化を選択することはあります。世間の情勢的には、国際化の枠組の中で地域化を行うのが流れなので、本書では最初の意味でのみ地域化の用語を使います。
Javaは言語として国際化の枠組を提供しています。適切にAPIを使って適切にプログラムを作る限りソフトウェアを自動的に国際化できます。しかし、枠組があっても、適切にプログラミングをしなければ(たとえば先の例に挙げたように日本語をハードコードするなど)国際化はできません。
出力メッセージが身近なので例に挙げましたが、他にも国際化の対象になる領域には次の一覧のようなものがあります。
国際化の対象領域 ・ 出力メッセージ ・ 入力方式 (かな漢字変換対応など) ・ 時刻や日付 (表記。タイムゾーン(時差)。異なるカレンダー(太陰暦など)) ・ 数値表現 (カンマの位置や小数点の記号などの表記) ・ 通貨 (単位。表記など)
時刻や日付や通貨など様々な表記上の処理を総称して「書式化(フォーマット処理)」と呼びます。
出力メッセージはソフトウェアごとに固有なので常に翻訳という地域化の作業が残ります。一方、タイムゾーン対応などはJava自体が地域化のための情報を提供しています。これは後の節で説明します。
■■18-1-1 データ交換と国際化
前節の国際化と地域化の話はひとつのプログラムに閉じた話でした。本書でJavaを勉強する人の中にはネットワークプログラミングをする人も多いでしょう。インターネットをはじめとして外部とやりとりをするプログラムにはデータ交換の必要があります。
データ交換で英語以外の言語を使う場合を考えてみます。これは、前節に述べた国際化よりも広い話になります。なぜなら、Javaの決め事だけで完結する話ではないからです。データ交換にはお互いの間での合意が必要です。一般にネットワークの世界ではお互いの合意を通信プロトコル(通信規約)と呼びます。Javaプログラムから見た外部とはネットワークだけではありません。Javaにとっては、Javaが動いているOSも外部世界です。たとえば、OSが提供するファイルシステムのファイル名やファイルの中身もJavaから見れば外部世界です。ファイルの中に書かれた日本語をどう解釈するかには、OSとの間の決め事が必要です。このような決め事の中でもっとも基本的な決め事が、言語を記述する文字に対する決め事です。総称してこれを文字コードと呼びます。
文字コードの話は国際化の話の中で陰に陽に現れます。次の節で文字コードに関して説明をします。
■■■18-2 文字コードとユニコード
■■18-2-1 文字集合
「文字と文字列」の章で文字に一意の数値を割り振ることを説明しました。現場のプログラミングでは、数値の割り振りの規則をユニコード(Unicode)と呼ぶことだけを知っていれば事足りることが多いでしょう。この章は文字コードをより深く理解するための話をします。
英語や日本語などの言葉は読み書きのための文字(記号)を持ちます。英語であればアルファベット26文字で、大文字と小文字を区別すると52文字です。日本語には平仮名、カタカナ、漢字の3種類あります。このような文字の集まりを「文字集合(character set)」と呼びます。文字集合をコンピュータで扱うにはそれぞれの文字に一意の数値を割り振ります。数値を割り振った文字集合を、数値の割り振り規則を含めて「コード化文字集合(coded character set)」と呼びます。コード化文字集合がコンピュータの文字の扱いの基本です。文字に割り振った数値を「コード値(ポイント)」と呼びます。
概念上、コード化しない文字集合を定義できますが(「英語アルファベット」や「平仮名」を考える時、それらは単なる文字集合でコンピュータと無関係に定義できる概念です)、コンピュータの世界ではコード化していない文字集合にあまり意味はありません。このため、文字集合(文字セット)の用語を使った場合は暗黙にコード化文字集合を意味します。本書でも、以降、単に文字集合という用語を使うと、それはコード化文字集合のことです。
■■18-2-2 ASCIIとISO 646
歴史的にコンピュータの世界では英語を扱うことが主流です。英語アルファベットの文字に数値を割り振る規則を決めるとコンピュータで英語を扱うことができます。たとえば、'a'に1、'b'に2のように割り当てていけば、英文字をコンピュータで扱うことができます。みんなが独自にコード化すると不便なので、数値の割り当て規則の標準化が行われました。それがASCII(American Standard Code for Information Interchange)です。ASCIIでは、英語アルファベットの大文字小文字に加えて数字や記号を含めてコード化しています。名称のとおりASCIIはアメリカの規格でしたが、後にISO 646として国際標準になりました(注1)。英語アルファベットと数字と記号を含めても、当初必要とされた文字数は127個以下でした。このためASCIIは7ビットの範囲で文字をコード化しています。
■■18-2-3 ISO 8859
コンピュータの普及とともに英語以外の言語をコンピュータで扱う要請が強くなりました。そのひとつの流れはヨーロッパ圏での動きです。ヨーロッパ圏には英語アルファベット以外のアルファベット文字を持つ国があります。ヨーロッパの各国がそれぞれのアルファベット文字をコード化しました。英語アルファベットを使う必要もあるので、結果的に、ASCIIを包含する形でコード化しました。各国のアルファベット文字の数はたいてい127個以下ということもあり、またコンピュータの世界は8ビット単位での処理が便利である背景もあり、ヨーロッパ圏の文字集合は8ビットでコード化するのが普通です。8ビットのコード化文字集合のうち、前半(最上位ビットがゼロ)はASCIIとほぼ等価で、後半(最上位ビットが1)に追加アルファベット文字をコード化します。これらのコード化文字集合はISO 8859として標準化されました(注2)。
■■18-2-4 日本語
日本語を適切に扱うためには平仮名とカタカナと漢字をコード化した文字集合が必要です(注3)。この文字集合の規格をJISとして標準化しました。JIS X 0201やJIS X 0208などと呼ばれる規格です。JISではよく使う漢字の集合を規定しています。ここには見落としがあったり、時代の要請による変化もあるため、JISの規格は数年おきに見直しが行われます。JISが改定されて新しい漢字が追加されたというニュースを読んだこともあるでしょう。改定年度を付与してJIS X 0208:1983(1983年度に改定)などと呼ぶこともあります。
漢字を含めた文字集合をコード化するには8ビット長で足りないことは明らかです。このためISO 8859のようにASCII上位互換にはなりえません。しかし、現実の世界は英数字にASCII文字を使うことが求められます。結果的に、英数字にはASCII文字、平仮名、カタカナ、漢字にはJISの文字、と混在する必要があります。
■■18-2-5 エンコーディング規則
歴史的および技術的な理由で、ASCII文字とJIS文字を混在させる方式が主に3つあります。EUC、Shift-JIS、ISO 2022-JPと呼ばれる3つの方式です。これらの方式の違いをエンコーディング規則(エンコーディングスキーマ)と呼びます。ASCIIのようにコード化文字集合のコードポイントをそのままエンコードに使える時には存在しない概念です(コード化=エンコーディング規則とも言えます)。
エンコーディング規則は日本語特有の概念ではありません。ISO 8859に複数のパートがあることを説明しました。それらを単純に混在させることはできません。なぜなら、たとえばISO 8859-1とISO 8859-2は同じコードポイントに対して異なる文字を持つからです。同様の理由で日本のJISの文字とISO 8859の文字を単純には混在できません。また、日本がJISで自国の文字集合をコード化したように、中国や韓国なども独自に自国の文字集合をコード化しています。これらは日本語のJIS文字と単純には混在できません。
■■18-2-6 ISO 2022
各国語のコード化文字集合を混在させる汎用的なエンコーディング規則としてISO 2022が標準化されました。ISO 2022はエスケープシーケンスにより異なる文字集合を切り替える方式です。エスケープシーケンスは文字集合の切り替わりを示すシーケンスです。次の図のような概念です。
▼図2.1 ISO 2022の概念 "abcあいうabc" iso 2022 jpでエンコード "abc" [][][] "あいう" [][][] "abc" エスケープシーケンス エスケープ文字 JIS文字に切り替わる指示 ASCII文字に切り替わる指示
ISO 2022-JPはこの枠組に則った方式です(ただしASCIIとJISの文字集合に限定したISO 2022のサブセット規則です)。ISO 2022は汎用的な仕組みゆえにプログラムから扱いにくい欠点があります。概念図を見ると文字数を数えるだけでも面倒そうなことが予想できるでしょう。
■■18-2-7 ユニコード
このような歴史的背景の下、ユニコード(Unicode)が生まれました。Unicodeは最初から全世界のコード化文字集合を目指しています。そのために8ビット長で足りないことは自明なので、16ビット長でコード化しています。文化的摩擦もありましたが(注4)、結果的に、UnicodeはISO 10646として標準化され、事実上の業界標準になりつつあります。
Javaは内部コードとしてUnicodeを採用しています。16ビット固定長でコード化されることで、文字数を数える処理などがISO 2022より単純になることが想像できるはずです。
Unicodeは16ビット長のコード化文字集合なので、コードポイントは16ビット長になります。インターネットやOSの世界のエンコーディング規則はASCIIとISO 8859-1が事実上の業界標準です。これらは8ビット長の世界なので16ビット長のUnicodeとの混在に問題があります(区別がつきません)。また、世の中の多くのプログラムは文字をバイト(8ビット)単位で扱う長い歴史があります(注5)。
このため、Unicodeを8ビット長の世界で扱うエンコーディング規則が生まれました。主なエンコーディング規則はUTF-8とUTF-7です(UTF:Unicode Transformation Format)。特にUTF-8はASCII上位互換で、かつ既存OSとの相性も良いためインターネットの世界で標準となりつつあります。
UTF-8は次の規則でエンコーディングをします。Unicodeの先頭の128個の文字集合はASCIIと一致しているので、UTF-8はASCIIの上位互換になります。UTF-8は可変長(文字ごとのバイト数が異なる)のエンコーディング規則ですが、各文字の先頭のバイト(のビットパターン)だけで文字のバイト数がわかります。このため文字数を数える処理などが(可変長エンコーディングの割には)比較的容易という利点があります。
▼図2.2 UTF-8のエンコーディング規則の説明 16bit 8bit 8bit 00000000 0aaaaaaa -> 0aaaaaaa 00000aaa aabbbbbb -> 110aaaaa 10bbbbbb 0aaabbbb bbcccccc -> 1110aaaa 10bbbbbb 10cccccc Unicode(UCS2) -> UTF8
UTF-8との対称もあり、16ビット固定長のエンコーディング規則をUTF-16と呼びます。後述するサロゲートペアを無視すると、UTF-16はUnicodeのコードポイントそのものです。Javaの文字(char)のエンコーディングはUTF-16です。
ここで話が終わらないところにUnicodeの不幸があります。JISの文字集合が改定を重ねて新しい漢字を追加するように、Unicodeも改定を重ねて新たな文字を追加しています(注6)。各国の要求に応えているうち必要な文字数が16ビット長でコード化できる数を越えました。このため、コード化のために32ビット長を使う決定をしました。しかし、この時点で既にUnicodeはある程度広まっていたため、16ビット固定長の世界をいきなり変更することはできませんでした。16ビット固定長を仮定していた世界にはJavaも含まれます。Unicodeが32ビット長になったからと言ってJavaの基本型であるchar型のサイズを今さら変更できません(変更すると互換性が壊れます)。現実と折り合いをつけるため、Unicodeの16ビット固定長の文字集合を基本文字集合という形で再定義して、32ビット固定長の文字集合と分けました(注7)。これらを区別するためにUCS-2(UCS:Universal Coded-Character Set)とUCS-4という用語を使います。2と4はバイト長を示しそれぞれ16ビット長と32ビット長です。なお、UTF-8はUCS-4に対しても同じ規則で適用可能であることを補足しておきます(UTF-8でエンコーディングすると最大で6バイトになります)。
UCS-4の文字集合をUTF-16でエンコードするためにサロゲートペアと呼ばれる仕組みが導入されました。サロゲートペアは可変長エンコーディング規則です。0xd800から0xdfffの範囲以外の文字は、そのまま16ビットで1文字です。0xd800から0xdfffの範囲をサロゲート範囲にして文字を割り当てません。その代わり0xd800から0xdfffの範囲はふたつの16ビット値で1文字になります。0xd800から0xdfffの範囲に2048個の数値があるのでサロゲートペアにより2048の2乗の数だけ追加の文字を表現可能です。切りの良い範囲で0x1ffffから0xfffffの文字をサロゲートペアで表記します。
▼図2.3 サロゲートペアの解説 (1) 0000 - ffff (d800-dfffを除く) UTF-16 xxxx xxxx xxxx xxxx 0000 - ffff そのまま16ビットで1文字 (2) 1ffff - fffff 0000 aaaa xxxx xxyy yyyy yyyy 1101 1000 aaxx xxxx 1101 11yy yyyy yyyy d d 8-b c-f UTF-16 d800 - dbff dc00 - dfff サロゲートペア (16ビット x 2 で1文字)
Javaでは、Java5でサロゲートペアに対応しました。この結果、Javaのchar型はUnicodeの文字と1対1に対応しなくなりました。サロゲートペアの場合、ふたつのcharで1文字になるからです。同じ理由でStringオブジェクトのlengthメソッドが返す値は厳密には文字数でなくなりました。lengthメソッドはUnicodeの単位数(コードユニットと呼びます)を返すと再定義されました(内部動作は変わっていません。従来はUnicodeの単位数が文字数と一致していただけです)。
サロゲートペアを気にする局面は当面限定されているはずです。サロゲートペアを無視すれば、従来どおり、Javaの文字はUnicodeのコードポイントと1対1に対応し、結果としてcharはUnicode文字と1対1に対応すると考えることができます。
ここまでの用語および補足する用語を表にまとめます。
▼表2.1 用語 コード化文字集合 一意の数値を割り当てた文字の集合 エンコーディング規則(エンコーディングスキーマ) コード化文字集合をコードのシーケンス化する時の規則 コードポイント コード化文字集合で文字に1対1に割り振った数値 コードユニット UTF-16のエンコーディング規則のJava文字の16ビット単位のコード(基本文字集合ではコードポイント=コードユニット)。補助文字ではふたつのコードポイント=2つのコードユニット charset インターネット(MIME由来)の世界ではエンコーディング規則を意味している コードページ IBMとマイクロソフトによるエンコーディング規則の規格
■■18-2-10 照合規則(collation)
国際化関連の特殊な用語のひとつに「コレーション(collation)」があります。日本語で「照合(規則)」と訳します。本書では照合の用語を使います。
照合とは文字の同定(同じ文字の判定)や順序(ソート規則)の比較のことです。一般に照合のために文字に数値を割り振ります。文字に数値を割り振る話はコード化文字集合の説明でもしました。コード化文字集合で文字に割り振った数値をコードポイントと呼びました。残念ながらコードポイントだけでは照合に不充分な場合があります。わかりやすい例は英語アルファベットの大文字と小文字の関係です。'a'と'A'には異なるコードポイントを割り振ります(異なる文字なので当然です)。大文字と小文字を同じ文字として扱ったり、あるいは大文字と小文字を区別せずにソートするには、コードポイントの比較だけでは不充分です(注11)。
現実的には、照合が大きく意味を持つのはヨーロッパ圏の言語です。ヨーロッパ各国のアルファベットには、英語の大文字小文字の関係のような規則が色々とあります。これらを扱うために照合が必要です。
日本語で該当するのは、ひらがなとカタカナを区別せずにソートしたい場合などです。また濁点や半濁点にも似た関係があります。たとえば、「は」「ば」「ぱ」を同じソート順にしたい場合などです。
残念ながら、日本語の文字に対する適切な照合は技術的にまだ成熟していません。Javaの標準的な技術で可能な照合は、ひらがなとカタカナのみです。漢字については、存在しない、というのが現実です。
コードポイントで比較したソートと照合で比較したソートで、結果が異なる例を示します。
▼リスト2.3 照合で比較したソート import java.util.*; import java.text.*; class My { public static void main(String[] args) throws Exception { String[] arr = new String[] { "かか", "がが", "カイ", "さ", "あいう", "うあい" }; Arrays.sort(arr); for (String s : arr) { System.out.println(s); } System.out.println(""); Collator coll = Collator.getInstance(Locale.JAPAN); Arrays.sort(arr, coll); for (String s : arr) { System.out.println(s); } } }
実行結果 $ java My あいう うあい かか がが さ カイ あいう うあい カイ かか がが さ
▼注1 英語アルファベットのコード化文字集合としてはASCIIが事実上の標準です。唯一、現在でも残る非ASCIIのコード化文字集合がEBCDICです。EBCDICはメインフレームの世界で残っています。Javaプログラマの多くは気にする必要がないため説明は割愛します。
▼注2 ISO 8859はパートに分かれていて、ISO 8859-1やISO 8859-2などがあります。ASCIIにそれぞれのヨーロッパ語アルファベットを追加したコード化文字集合です。特にISO 8859-1はフランス語アルファベット、ドイツ語アルファベット、イタリア語アルファベットなど主要な西ヨーロッパ言語のアルファベットを含む文字集合なので、Unicode以前の事実上の業界標準文字集合です。
▼注3 昔、ISO 8859のようにASCIIにカタカナを追加して8ビットで日本語をコード化する文字集合がありました。平仮名ではなくカタカナだったのは、当時の画面の解像度が低かったためです。
▼注4 興味のある人は「ハンユニフィケーション」などで調べてください。
▼注5 「文字と文字列」の章の「バイトと文字」を参照してください。
▼注6 Unicodeの場合、改定年度ではなくバージョン番号がつきます。
▼注7 基本文字集合以外の文字を補助文字と呼びます。
▼注11 コードポイントに順序の概念は必須ではありません。文字に1対1対応の数値を割り振るだけだからです。順序を持つのはコレーション値です。ただし、現実のコードポイントはたいてい順序の概念を持ちます('a'に'b'より大きなコードポイントを割り振る意味はないので)。