Javaの最近のブログ記事

最近のJavaの更新版のリリースノートを見ていたら、こういうバグが修正されているのが目にとまりました。

XML文書の構造を変換するXSLTの実行において、部分文字列を得る関数にBMP外の文字を与えると結果が正しくないという話です。サロゲート1個を1文字分として勘定した結果になってしまうというものです。今年の3月に報告されています。

いかにもありそうなバグです。2015年になってもまだこういうバグが出てくるのだなあという感想を持ちました。

先日、拙著「プログラマのための文字コード技術入門」のEPUB版に関して、Unicodeで結合文字を用いる必要のある文字がEPUBの処理環境でうまくなくてPDF版よりも提供が大幅に遅れたということを書きました。サロゲートだけでなく結合文字についても、というか多分こちらの方がより長く(あるいは多く、またはその両方)、トラブルが続きそうに思えます。

悲観的にいえば、Unicodeあるいはその処理環境が今と同じ仕組みであれば、こうした種類のトラブルは半永久的に続くのかもしれません。

プログラムのソースファイルに文字を記すときに、キーボードから入力できなかったりフォントがなかったりといった理由で、Unicodeの符号位置を使って記したいことがあります。最近の言語では似た形式でそうしたUnicodeエスケープを記述できますが、微妙に違いがあります。ここではJavaとPython, Rubyを比べてみます。

JavaのUnicodeエスケープ

Javaではバックスラッシュ(\)とアルファベットのuに続けて4桁の16進数でUnicode符号位置を記します。

例えば、\u4e00 とすれば漢字の「一」を記したのと全く同じことになります。

JavaのUnicodeエスケープはコンパイルの初期段階で処理されるものなので、文字列リテラルの中で改行を意味する \n 等とは扱いが異なることに注意が必要です。このことは『プログラマのための文字コード技術入門』第7章に記しました。

'\u' の後に記す符号位置は厳密に4桁しか解釈されないので、BMP外の符号位置は直接記すことができません。サロゲートペアの上位下位をそれぞれ \u を使って記す必要があります。これは面倒な話で、いまだにこれしかないというのが信じられないのですが、Java 8版のJava Language Specificationをざっと見たところではほかの手段はなさそうです。(見落としてるかなぁ...)

PythonのUnicodeエスケープ

Pythonにも同様の記法があります。'u' の大文字小文字2種類があります。小文字の \u は16進4桁、大文字の \U は16進8桁で記します。例えばこうなります:

>>> print u"\u9bd6"
鯖
>>> print u"\U00029e3d"        ←BMP外の符号位置、魚の「ホッケ」の漢字
𩸽

(Python 2.7.5で動作確認)

ここで横着して頭のゼロ3つを省いてしまうとエラーになります。8桁というのは無駄な感じもしますが(ISO/IEC 10646の符号空間は以前は4バイト分ありましたが、今はUTF-16の範囲に限定してしまったので6桁で十分)、Javaと違ってサロゲートの値を使わずに済むのはいいですね。

RubyのUnicodeエスケープ

Ruby 1.9以降では、 \uXXXX という形と、波括弧を付けた \u{XXXX} という2種類が使えます。前者は4桁限定、後者は5桁・6桁でもOKです。

irb(main):013:0> print "\u9bd6"
鯖=> nil
irb(main):014:0> print "\u{29e3d}"
𩸽=> nil
irb(main):015:0> print "\u{9bd6 29e3d}"
鯖𩸽=> nil

(Ruby 2.0.0で確認。nil云々は無視して結構です)

波括弧を付けない記法で例えば \u29e3d のように5桁記してしまうと、"\u29e3" + "d" のように扱われてしまいますから、こうしたときは波括弧を用います。

最後の例のように、波括弧を使う記法ではスペースで区切って複数の符号位置を並べることができます。

Javaの文字列関係のAPIはJDK 1.5 (Java 5)でサロゲートペア対応のために大きく変わりましたが、その後、Java 7でも地味にCharacterクラスにメソッドが追加されていたりします。

  • public static boolean isBmpCodePoint(int codePoint)
  • public static boolean isSurrogate(char ch)
  • public static char highSurrogate(int codePoint)
  • public static char lowSurrogate(int codePoint)
  • public static boolean isAlphabetic(int codePoint)
  • public static boolean isIdeographic(int codePoint)
  • public static int compare(char x, char y)
  • public static String getName(int codePoint)

大体メソッド名から推測できるものが多いです。

注意が必要な点として、isAlphabetic(int)は日本人が「アルファベット」と聞いて真っ先に思い浮かべるラテンアルファベットだけでなく、ギリシャやキリルも、それどころか平仮名や漢字さえ含んでいます。記号類でない言語表記のための文字が大体相当すると思っていいでしょう。定義はメソッドの説明に記されているので、用途に応じて便利なケースがあれば使えばよいでしょう。ちなみにカテゴリMnは含んでいないので、結合文字は(例えば合成用の濁点やアクセント記号等) falseになります。

isIdeographic(int)は漢字かどうかを判定します。一部の人がすぐに気にしたであろう互換漢字もこれでtrueを返します。「々」を与えるとfalseになります。

getName(int)は普通のアプリケーションでは使わないかもしれませんが、一部の人(?)は歓迎しそうです。Character.getName(0x3042) とすると "HIRAGANA LETTER A" が得られます。

メソッドでないのですが、Character.UnicodeScriptというクラスが新たに定義されています。これは Unicode Standard Annex #24: Script Names という文書に記されている属性定義を利用できるものです。

例えば、

Character.UnicodeScript s = Character.UnicodeScript.of(0x3042); // 「あ」

とすると、enum定数のHIRAGANAが得られます。

では平仮名と片仮名の両方で用いられる長音符号「ー」はどうなのかというと、これは上記の文書の定義に従って「COMMON」になります。ちなみに合成用の濁点(結合文字)はINHERITEDというものになります。前の文字に同じ、という意味のようです。

漢字は定数HANです。「々」もHANになります。

便利そうな感じはしますが、定義に気をつけて良く吟味しないと期待通りに使えないかもしれないので注意ですね。Unicode Character Databaseの一部として用意されているデータファイルから各文字の値が得られます。

Javaで動くウェブアプリケーションフレームワークであるPlay Framework 1.2.3の上で動いているプログラムを見てちょっといじっていたら、文字コードの扱いに関して、予想と異なる挙動をしていることに気付きました。

そのアプリではJavaのReader/Writerクラスを使う際に文字コードを指定しないでいたので、デフォルトエンコーディングとして、プラットフォームのデフォルト、したがって日本語版WindowsなのでシフトJIS (の、ベンダ定義外字付きのもの。JavaではMS932というラベルがついている) になるはずだ、と思っていました。

ところが、UTF-8の文字列データがきているはずのストリームを、文字コード指定なしにReaderで読んできちんと日本語文字列になっている。あれれどうなってるんだろう。

しばし悩んでしまいましたが、実はPlay起動時にjavaコマンドのオプション指定 -Dfile.encoding=UTF-8 というのが付加されていて、デフォルトエンコーディングが強制的にUTF-8になっていたのでした。

私はPlay Frameworkに詳しくないので、もしかしたらドキュメントのどこかに書いてあって私がそれを見逃していたのかもしれません。が、これはちょっと勘違いしやすそうだなと思いました。

ちなみに、Play 1.2.2のドキュメントには、国際化について、文字コードの問題は面倒くさいのでUTF-8しかサポートしないという意味のことが書かれていました。これはソースファイルや出力HTML等のことをいっているわけですが、このjava起動時の設定もその一環なのかもしれません。

入力としてUTF-8のテキストファイルをとるJavaプログラムでうまくいかないことがありました。

テキスト形式で入力されたデータを処理するプログラムなのですが、ファイル中に存在するはずのデータがないといってエラーになる。

テキストエディタで開いても、ExcelやLibreOffice Calcで開いてみても、ファイルに異常は見当たらないし、問題のデータもきちんと記述されているようにしか見えない。

実はこのエラーの原因は、入力のテキストファイルにBOMが付いていることでした。BOMがどういうものかは『プログラマのための文字コード技術入門』をご覧ください。

Javaで書かれた処理プログラムがUTF-8のテキストを読み込む際に、BOMを消費せずに単なるUnicode文字のように扱うため、1行目のデータの先頭にゴミが付いた状態になっていたのです。それで、見えないゴミ付きのデータになってしまい、意図どおりに動作しないし、テキストエディタで開いてみても異常に気付かなかったわけです。

JavaのライブラリでUTF-8のテキストを読み込むとき、先頭にBOMがあると、それを単なるU+FEFFという符号位置の文字データとして読み込んでしまいます。本来のBOMは、Byte Order Markという名前のとおり、バイト順を示す役割のものであって、文字を符号化したデータの一部ではありません。UTF-16/32と違ってバイト順の問題のないUTF-8にBOMを付けるべきかという問題は議論が分かれるところかもしれません。しかし現実にはBOM付きUTF-8は生成されることがあります。

ウェブ検索で「java utf-8 bom」などとしてみると、BOM付きUTF-8をJavaで読み込んでトラブルになった話が複数出てきたり、また互換性のためにJavaではUTF-8読み込み時の挙動を変える予定はなさそうだという話が出てきたりします。

自分で対策をするしかないようです。

ちなみに、iconvのUTF-8も同じようにデータ先頭のBOMを単なる文字として扱うということを、以前当ブログの記事に書きました。

2つ前の記事では、ScalaプログラムからShift_JIS-2004のテキストデータを読み込んでUTF-8にして出力してみました。これで、Shift_JIS-2004を読み込めることが分かったわけです。

では、Shift_JIS-2004で出力する方はどうか。おそらく問題ないのでしょうけど、一応やってみないことには、どんな落とし穴があるか分かりません。

文字コードとしてShift_JIS-2004を指定してファイルを読み込むよう開き、1行読み込んではまたShift_JIS-2004として別のファイルに書き出してみます。内部コードがUnicodeなので、いったんUnicodeに変換したのちに元のSJISにするということになります。往復変換 (round-trip conversion) がうまくいくかを見たいわけです。

import scala.io.Source
import java.io.PrintWriter

val inputFile = Source.fromFile("hoge.sjis.txt", "x-SJIS_0213")
val outputFile = new PrintWriter("hoge-out.sjis.txt", "x-SJIS_0213")

try {
    for (line <- inputFile.getLines) {
        outputFile.println(line)
    }
} finally {
    inputFile.close
    outputFile.close
}

今回は前回と違って行番号なしで、元のまま出力します。これを実行した後で、

diff hoge.sjis.txt hoge-out.sjis.txt

とすると、差分なし、つまり綺麗に元通りに戻ったということでした。

念のため、常用漢字人名用漢字のCSVのSJIS版について同じことをして、うまくいくことを確認しました。

全部の文字を試したわけではないけれども、問題の起こりそうなポイントはおさえているので、多分大丈夫でないかと思います。

ちなみに、Shift_JIS-2004に対応している言語はほかにはPythonやGaucheがあります。これらはEUC-JIS-2004にも対応しています。また、OSでいうとMac OS XはShift_JIS-2004に対応しており、OS添付のテキストエディタでも扱うことができます。

Java 7ではShift_JIS-2004への対応が追加されています(OracleのサイトのSupported Encodings参照)。前から気付いてはいたのですが、今まで何となく試す機会がないままいました。

文字コードの名前として "x-SJIS_0213" という文字列で登録されています。文字コードを指定できる場面でこの文字列を指定してやればOKです。ご承知のようにJavaの内部コードはUnicodeなので、この場合の対応というのはUnicodeとのコード変換が実装されているということです。

Javaで対応したということは、Scalaでも使えるんだろうと思って、Scalaで簡単なプログラムを書いて試してみました。

// ファイルからテキストを読み込んで別のファイルに行番号をつけて出力する
import scala.io.Source
import java.io.PrintWriter

val inputFile = Source.fromFile("hoge.sjis.txt", "x-SJIS_0213")
val outputFile = new PrintWriter("hoge-out.utf8.txt", "UTF-8")

try {
    var i = 1
    for (line <- inputFile.getLines) {
        outputFile.println(i + ": " + line)
	i += 1
    }
} finally {
    inputFile.close
    outputFile.close
}

hoge.sjis.txtという名前のShift_JIS-2004のファイルを読み込んで、行番号をつけた上でUTF-8にしてhoge-out.utf8.txtというファイルに出力します。

試しに作ったhoge.sjis.txtを同じディレクトリに置いておいて scalaコマンドで上のプログラムを実行すると、hoge-out.utf8.txtが作成されます。Scala 2.10.0とOpenJDK 1.7.0をインストールしたUbuntu Linux 12.10で試しています。

出力ファイルを見たところでは期待通りに動作しているようです。以下のようなファイルができます。

1: test file.
2: あいう
3: αβ
4: 鷗     # 第3水準、BMP、一般名詞
5: 米芾   # 第3水準、BMP、人名
6: 褚遂良 # 第3水準、BMP、人名
7: 蔡邕   # 第3水準、BMP、人名
8: 𩸽  # 第4水準、SIP、一般名詞
9: 渞  # 第4水準、BMP、名字
10: 氵  # 第4水準、BMP、字体記述要素
11: 剝離  # 第3水準、BMP、サ変名詞、JIS2004追加、常用漢字
12: 𠮟咤  # 第3水準、SIP、サ変名詞、JIS2004追加、常用漢字
13: 神  # 第3水準、BMP、互換漢字
14: ㇰ  # アイヌ語用片仮名
15: ㇷ゚  # アイヌ語用片仮名、要結合文字
16: 〜  # WAVE DASH U+301C

なお、Java 6でも、途中のUpdateから対応しています。現時点の最新版では使えるはずです。

個人的には、JIS X 0213の符号化方式としてはEUC-JIS-2004に対応してくれた方が嬉しいのですが、SJISももちろん必要な措置ではあります。

プログラムの中の変数名やメソッド名、関数名、クラス名といった識別子には英数字のみを使うことが多いですが、Java言語では漢字や平仮名・片仮名などを使うこともできます。変数名を「年齢」だとかメソッド名を「賞与計算」だとかいう風にすることもできるわけです。

Java言語仕様の中で識別子に使える文字が規定されています。もっとも、使える文字の一覧がこの言語仕様書に示されているわけではなく、実際にはCharacter.isJavaIdentifierStart(int) と Character.isJavaIdentifierPart(int) がtrueを返す(前者が識別子先頭、後者が2文字め以降に対応)文字という風に、Characterクラスに丸投げしています。

どんな文字が使えるのかはAPIリファレンスのCharacterクラスの上記メソッドの説明にざっくり書かれています。簡単にいえば、記号類でなく言語表記のための文字はなんでもいけます。ラテン文字はもちろん、ギリシャ文字、キリル文字、漢字、平仮名、などなど。数字も(2文字め以降は)もちろん大丈夫。

分かりにくいのは記号類です。通貨記号はいいとして、「連結句読点文字」(connecting punctuation character)というあまり一般的と思われない用語が素っ気なく使われています。例として '_' が挙げられていて、これが「連結句読点」かといわれればそんな気もしますが、ほかに何が該当するのかこれで分かる人は少ないのではないでしょうか。ここで意味するところはUnicodeDataでいうカテゴリConnector Punctuation (省略形Pc)に相当するらしく、ほかにはU+203F UNDERTIEなど少数があるようです。

さて、この前ちょっと調べ物をしているとき、これに関連して、興味深いブログ記事をふとしたはずみで見つけました。

Java 6まではJIS X 0208にある中黒(中点)「・」をメソッド名の中に使えていたのに、Java 7になったらコンパイルエラーになった。これはどうしたことだと。

このブログで説明されているところでは、Javaの言語仕様が変わったわけではなく、Unicodeの文字カテゴリの定義で、中黒は上で触れた「Pc」として以前は分類されていたのに、Java 7の対応するUnicodeのバージョンでは「Po」(Other Punctuation)に変更になったと。このため、以前は中黒を識別子に使うことができたのに、Java 7ではできなくなったというのです。このブログ記事は大変丁寧に調査・説明されています。

文字コード表の例示字形に変更がなくても、属性の定義の変更によって、影響があり得るわけですね。

ちなみに、Java 6の文字情報はUnicodeバージョン4.0、Java 7は6.0に基づいています。APIリファレンスのCharacterクラスのところに記載があります。また上記ブログによると、中点のカテゴリがPcからPoに変更されたのはUnicode 4.1だそうです。

Java 6が対応しているUnicodeのバージョンは4.1だそうです 【追記: 後で確かめたら4.0だそうです。私の思い違いでした _o_】。IVSに使われる面0Eのvariation selector (異体字セレクタ)はこのバージョンには既に入っています。なので、最新IVDの知識は期待できないとしても、variation selectorを無視するくらいの処理はひょっとしたら入っていてくれないかな、と思ってちょっと試してみました。

まず、Stringクラスの挙動から。まあ、何が起こるかは大体想像できるのですが、一応確認しておきましょう。

こんな風な文字列があるとします。

    String nonIvs = "与太郎";
    String ivs1 = "与\uDB40\uDD00太郎";
    String ivs2 = "与\uDB40\uDD02太郎";

ここで、\uDB40\uDD00というのはU+E0100の、\uDB40\uDD02というのはU+E0102のエスケープ表現です。つまり、この3つの文字列はいずれも「与太郎」という文字列を表しています。ivs1とivs2の「与」にはそれぞれU+E0100とU+E0102という異体字セレクタがついていますが、現在のIVDによるとこれはいずれも常用漢字体の (つまり、普通の)「与」の形であり、見た目に区別がつきません。つまり、この三つの文字列はIVS対応の環境で表示するといずれも「与太郎」と見えます。

この文字列をまずはString#equals()で比較してみましょう。

    System.out.println("\"" + nonIvs + "\".equals(\"" + ivs1 + "\") = "
        + nonIvs.equals(ivs1));
    System.out.println("\"" + ivs1 + "\".equals(\"" + ivs2 + "\") = " +
        ivs1.equals(ivs2));

結果はこうなります。

    "与太郎".equals("与[0E0100]太郎") = false
    "与[0E0100]太郎".equals("与[0E0102]太郎") = false

ただし、[0E0100] は、U+E0100のバイト列を表します。私の実験環境では四角の中に 0E0100 という文字が入った格好で表示されています。

これは想像通りですね。単にcharの列を比較しただけの結果になっています。つまり、見た目の区別がつかなくても容赦なく別々の文字列として扱っています。これが何をもたらすかというと、Stringクラスで比較しているプログラムに対しては、"与" と "与[0E0100]" と "与[0E0102]" を、見た目の区別がないにもかかわらず、人が入力し分けてやらないと困ることになるということです。

String#compareTo() も試しておきましょう。比較のためにもう一つ文字列を定義しておきます。

    String nonIvsNext = "与那国";

「那」はUnicode順でもJISコード順でも「太」より後になります。つまり、「与太郎」と「与那国」を比較すると「与那国」の方が後にくることが期待されます。実際、IVSを含まない文字列同士での比較、

    System.out.println("\"" + nonIvs + "\".compareTo(\"" + nonIvsNext + "\") = " +
        nonIvs.compareTo(nonIvsNext));

を実行すると、負の値が出力されます。これは期待通りです。

IVSが絡むとどうなるか。

    System.out.println("\"" + nonIvsNext + "\".compareTo(\"" + ivs1 + "\") = " +
        nonIvsNext.compareTo(ivs1));

を実行すると、

    "与那国".compareTo("与[0E0100]太郎") = -1910

と、負の値になります。「与那国」の方が「与[0E0100]太郎」より先だといっているのです。これは、見た目の期待に反します。残念。(まあ、Stringクラスにそこまで期待しない方がいいのですが)

気をとりなおして、Collatorでうまいこと処理してくれないものか、実験してみます。

JavaにはCollatorというものがあります。存在理由や効果などは『プログラマのための文字コード技術入門』の第7章を参照してほしいのですが、これを使うと文字コード順でなく言語に応じた適切な照合(collation)を実現できます。

準備として、デフォルトロケール (この場合は日本語環境) のCollatorオブジェクトを取得しておきます。

    Collator c = Collator.getInstance();

このCollatorで比較するとどうなるか。人が同じと認識する文字列は同じだという結果を返してほしいわけです。

    System.out.println("Compare " + ivs1 + " with " + nonIvs + ": " +
        c.compare(ivs1, nonIvs));

を実行すると、

    Compare 与[0E0100]太郎 with 与太郎: 1

となります。等しい文字列のときには0という値が返却されるのですが、ここで1と出ているのは、「与[0E0100]太郎」の方が大きい文字列だといっているわけです。念を押すようですが、見た目の区別はつきません。

IVS同士だとどうか。

    System.out.println("Compare " + ivs1 + " with " + ivs2 + ": " +
        c.compare(ivs1, ivs2));

この結果はこうです。

    Compare 与[0E0100]太郎 with 与[0E0102]太郎: -1

「与[0E0100]太郎」の方が小さいといっています。同じとはみなしてくれません。この二つは、見た目の区別のつかないIVS同士です。

どうも、Collatorを使っても、異体字セレクタを無視して比較するような賢いことはしてくれず、単に漢字の後ろに面0Eの文字があるような処理がされています。

だめおしにもうひとつ。

    System.out.println("Compare " + nonIvs + " with " + nonIvsNext + ": " +
        c.compare(nonIvs, nonIvsNext));
    System.out.println("Compare " + nonIvsNext + " with " + ivs1 + ": " +
        c.compare(nonIvsNext, ivs1));

この2行の実行結果は下のとおり。

    Compare 与太郎 with 与那国: -1
    Compare 与那国 with 与[0E0100]太郎: -1

IVSなしの「与太郎」と「与那国」では常識的に負の値が返却されています。一方、「与那国」と「与[0E0100]太郎」とでは、前者の方が小さいといっています。見た目の文字列としては「与那国」と「与太郎」の比較なので正の値がかえってほしいところですが、そうはなってくれません。もし「与太郎」(IVSなし)「与那国」「与[0E0100]太郎」の三つの文字列をソートすると、Collatorを使ったとしても、異体字セレクタの入るものが一番後にくるという格好悪い結果になってしまいます。見た目のソート結果としては「与太郎」「与那国」「与太郎」という風になり、なぜ「与那国」の後に「与太郎」がひとつだけ来ているのか、人が見てもさっぱり分からないという状況になります。

結論としては、予想されたことではありますが、Java 6はIVSについて特別なことは何もしてくれません。なので、IVSを含むデータを考えなしに垂れ流すと、いろいろ困ったことになるでしょう。UTS #37のIVDの仕様には「variation selectors are default ignorable」と書いてありますが、Javaはignoreしてくれないようです。

文字列がIVSを含む場合の照合(collation)についてUnicodeの人が何も考えていないとは思えないのですが、UTS #10 「Unicode Collation Algorithm」には異体字セレクタへの言及が何もありません。このあたりがどうなっているのか、もしご存じの方がいたら教えていただけるとうれしいです。(まあ、variation selectorは単に無視せよということなのかもしれませんが、どこかでそう明言されていないと実装者は何も考えなさそうな気がするので)

Ubuntuのパッケージマネージャ(Synaptic)からJava 6 SDKをインストールしたのになぜかJava DBが入っていないように見えるので頭の上に疑問符をいくつも浮かべていたのですが、このブログに書かれているように実はsun-java6-javadbという別パッケージになっていたのでした。

このパッケージをインストールすると、/usr/lib/jvm/java-6-sun/db に一式が入ります。この中のlibにderby.jarが入っています。

広告