Javaの最近のブログ記事

前の記事で,Pythonの識別子のUnicodeの扱いについて取り上げました。では,同じくUnicodeの様々な文字を識別子に使えるJavaとはどのように類似しているか,あるいは違っているでしょうか。

Javaの識別子の仕様

Javaは最初からUnicodeを前提に設計されており,変数名やメソッド名等の識別子にも世界の様々な文字を使えるようになっています。

識別子の仕様は言語仕様Java Language Specification 3.8. Identifiersに見えます。が,読んでみると実は,このセクションにはほとんど中身がなく,実質的にはJava APIのCharacterクラスの2つのstaticメソッド isJavaIdentifierStart(int) および isJavaIdentifierPart(int) にて,識別子に使える文字が規定されています。前者が識別子先頭,後者が2文字目以降です。

前者のメソッドのAPIドキュメントにはこう記されています:

  • isLetter(codePoint) が次を返す: true
  • getType(codePoint) が次を返す: LETTER_NUMBER
  • 参照される文字が通貨記号である ('$' など)
  • 参照文字が連結句読点文字である ('_' など)

一方,後者のメソッドつまり2文字目以降の定義は次のように記されています:

  • 汎用文字である
  • それが通貨記号である ('$' など)
  • それが連結句読点文字である ('_' など)
  • 数字である
  • 数値汎用文字である (ローマ数字文字など)
  • 連結マークである
  • 非スペーシングマークである
  • 文字の isIdentifierIgnorable(codePoint) が true を返す

少々とっつきにくく見えるかもしれませんが,Unicodeの文字カテゴリを前提としているので,その知識があれば「あれのことだな」と思いあたるでしょう。例えば最初の「汎用文字」はカテゴリLo, Luなどをまとめたもの,「数字」はカテゴリNd,「非スペーシングマーク」はMnといった具合です。最後の項目のメソッドは,制御文字の類いかどうかを判定するものです。

Pythonの識別子

Python3では,言語仕様の2.3. Identifiers and keywordsにて識別子の字句構造が定義されています。形式的に定義されているところを引用します:

identifier   ::=  xid_start xid_continue*
id_start     ::=  <all characters in general categories Lu, Ll, Lt, Lm, Lo, Nl, the underscore, and characters with the Other_ID_Start property>
id_continue  ::=  <all characters in id_start, plus characters in the categories Mn, Mc, Nd, Pc and others with the Other_ID_Continue property>
xid_start    ::=  <all characters in id_start whose NFKC normalization is in "id_start xid_continue*">
xid_continue ::=  <all characters in id_continue whose NFKC normalization is in "id_continue*">

Pythonの仕様書では,識別子の仕様は Unicode Standard Annex UAX-31 "Unicode Identifier and Pattern Syntax" に準拠していることが明示されています。これは一般的な言語の識別子の仕様(の雛形)として利用できるものです。Unicode仕様があまりに巨大なので,言語設計者の役に立つようなガイドラインとして作られたものだろうと思います。

実はJavaの方も内容的にはこれに準じているように見えます。ただし仕様書から参照されてはいません。

というわけで,JavaとPythonとで使える文字にあまり違いはありません。通貨記号の扱いが異なるのと,連結句読点(カテゴリPc)かアンダースコアのみかの違い,またUAX-31で互換性のために用意されている "Other_ID_{Start|Continue}" を含むか否かのような細かな出入りはあるようですが,大筋では大体同じものが使えると思ってよさそうです。

正規化の問題

ただし,JavaとPythonの小さくない違いとして,Pythonの方はUnicodeのNFKC正規化にて同一性をチェックされるということがあります。つまり,識別子の文字に,「é」のような文字を含む際に,「e + 合成用アクセント記号」としているか,あるいは合成済みの「é」の符号位置を用いているかという違いの取り扱いです。Pythonではこれらは正規化されて同じものとみなされます。

Javaにはこの規則はありません。Javaは正規化の仕様ができる前のUnicode仕様に基づいて設計されたので,比較的最近Unicode対応したPythonとは事情が異なります。

Javaで以下のようなコードを書いてみると実験できます。

public class JavaIdentifierTest {
    public static void main(String[] args) {
	int é = 0;
	int e\u0301 = 1;
	System.out.println(é);  // -> 0
	System.out.println(e\u0301);  // -> 1
    }
}

コード中のUnicodeエスケープ \u0301 は合成用アキュートアクセントです。JavaのUnicodeエスケープはコンパイルの早い段階で処理されるので,文字列リテラル専用ではなく,このように識別子の一部に用いることもできます。2つ変数を定義しており,どちらも同じ「é」を表しますが,片方は合成済みの符号位置,もう片方は結合文字を使っています。可能であれば,Unicodeエスケープを用いずに「基底文字e + 合成用アキュートアクセント」をエディタから入力してみても良いでしょう。Javaではこの2つの変数は同一視されずに別物として扱われます。

Pythonで同じことを試そうとすると,エスケープでなしに合成列をどうにかしてターミナルにペーストしてやる必要があります。私のMacの環境では,pythonコマンドでインタプリタを起動して,

>>> print('e\u0301')

として出力された é をマウスで選択してコピーすると,「e + 合成用アクセント」としてコピーできるようです (どこかで勝手に正規化されないことを祈りましょう)。確かめるには,

>>> len('é')
2

のように長さが2になっていればOK。これを変数名として,

>>> é = 1

のように変数として用いてみます。一方,キーボードから普通に入力したéは合成済みの符号位置になるので,Macの場合なら入力方式「ABC - 拡張」から Option-e e と打鍵して,

>>> é
1

とすることで,さっき代入した値が出てくることがわかります。つまり,文字éの符号化として合成済みの単一の符号位置を用いても,結合文字を用いて2つの符号位置で符号化しても,識別子としては同一のものとして扱われていることがわかります。

Java 9では国際化機構で用いられるリソース文字列のファイル表現の文字コードとしてUTF-8がデフォルトで使用されることになるそうです。従来、ISO/IEC 8859-1がデフォルトであるためにUnicodeエスケープが必要となり、外部ツールで日本語テキストを「\u3042」のようなエスケープ文字列に変換する煩わしさがありましたが、ようやく解消されることになります。

Javaには古くから国際化のための枠組みが用意されています。その最も基本的な機構となる、多言語のメッセージ文字列を用意する仕組みとしては設定ファイルなどに用いるプロパティファイルという形式が用いられています。ところがこのファイルはデフォルトの文字コードがISO/IEC 8859-1という西欧向けの1バイトコードなのでした。

このため、JDKではnative2asciiというツールが提供されて、Shift_JISやEUC-JP等の文字コードからUnicodeエスケープを用いる形式に変換できるようにされていました。また統合開発環境のEclipseにはプロパティエディタというプラグインが開発され、コマンド操作なしにあたかも直接漢字を記述できるかのような操作が可能になっています。とはいえリソースファイル自体はもちろんUnicodeエスケープなので、普通のテキストエディタで表示すると欧文以外は全く読めないものになります。

Javaのプロパティファイル自体はUTF-8で記述されても読み込めるように既になっていたのですが、その重要な用途である国際化のリソースファイルとして読み込むResourceBundleクラスではなぜかISO/IEC 8859-1がデフォルトという状態が長く続いていました。実は自分でクラスを定義して一工夫してやるとUTF-8にできるという裏技(?)もあったりしたのですが、そういうのはデフォルトで提供してほしいものです。(この辺のことは拙著『プログラマのための文字コード技術入門』第7章に記しています)

オープンソースのWebアプリケーションフレームワークであるPlay Frameworkではリソースファイルにわざわざ別の仕組みを用意してUTF-8で直に書けるようにしています。リソースファイルがもっと早くUTF-8で符号化できるようになっていたらこのような措置はとられれなかったかもしれません。

遅きに失した感はあれども、ともかく改善されることになって良かったです。

最近の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 でIVSを比較すると何が起こるか
Java 6が対応しているUnicode…
UbuntuではJava DBは別パッケージになっている
Ubuntuのパッケージマネージャ(Sy…

広告