Java による Unicode サロゲートプログラミング

Comments

はじめに

Java言語は 1995年に発表されたオブジェクト指向プログラミング言語です。設計当初から、内部文字コードに 16ビット固定長の Unicodeを採用したので、文字列処理を簡潔に記述することができました。ところが、同時期に Unicodeコンソーシアムでは、将来収録予定の文字に対して、16ビット固定長で表現できる 65536個の領域では不足するという問題が議論になりました。この問題を解消するために、1996年に制定された Unicode 2.0から、サロゲートペア(Surrogate Pair)が導入されました。サロゲートペアとは、16ビット表現できる領域に、ハイサロゲート(High Surrogate)を1024個、ローサロゲート(Low Surrogate)を1024個定義し、両者を組み合わせることで、約100万個(=1024x1024個)のコードポイントを表現する方式です。それ以外の領域には変更はなく、従来通り 16ビットで表現されます。この方式は UTF-16と呼ばれ、1個のコードポイントを表現するために、16ビットと 32ビットの表現形式が混在することになり、文字列処理が複雑になりました。

符号化方式は定義されたものの、実際に文字が定義されたのは、2001年に制定された Unicode 3.1からです。それまでの 6年間は Java言語もサロゲートペアを意識する必要がなく、サロゲート未対応のまま、1.0から 1.4にバージョンアップされました。1.4 が発表されたのは、実に 2002年で、実際の文字が定義された後でした。ようやく、1.4の次期バージョンからサロゲート対応が考慮され、2004年に発表された 1.5で本格的なサロゲートAPIが提供されました。その後、基幹業務に使われるプロダクトでは徐々にサロゲート対応が進んできましたが、サロゲートペアで表現された文字は使用頻度が低いことから、通常のプログラムでサロゲートペアが意識される状況は、依然として多くありません。

ところが、日本では、2010年告示予定の改定常用漢字表にサロゲートペアで表現された文字が収録される可能性が出てきました。改定常用漢字表は、教育現場でも使われる基本的な漢字セットです。したがって、日本語を使用するすべてのプログラムでサロゲート対応が必要になってきます。同様に、JIS X 0213で定義された漢字にもサロゲートペアが必要なものが多くあり、その対応は急務です。また、成長著しい中国市場でも、GB18030で定義された漢字を使用するためにサロゲートペアを無視することはできません。これらの状況から、今後はすべてのプログラムでサロゲート対応が必須になると思われます。

Java言語がサロゲートAPIを初めて提供した 2004年から既に 6年の歳月が経過しましたが、サロゲートAPIを使用した定型処理や、サロゲートAPIを使用することによる処理速度の評価について、まだまだ十分な情報が蓄積していません。この記事は、それらの情報を提供するのが目的です。記事を読んだあとに、すべてのプログラマにとってサロゲートAPIが身近なものになれば幸いです。

  • この記事で言及する Java 1.4、Java 1.5は、それぞれ「Java™ 2 Platform, Standard Edition, v 1.4.x」、「Java™ 2 Platform Standard Edition 5.0」のことを示します。
図1. 文字例: UTF-16表現値と例字形
図1. 文字例: UTF-16表現値と例字形
図1. 文字例: UTF-16表現値と例字形

サロゲートAPI の使用方法と評価

順次アクセス

文字列処理の第一歩は、先頭から順番に文字にアクセスして、各文字に対して、必要なアクションを行う順次アクセス処理です。ここでは、サロゲートペアを含む文字列から文字を取り出し、32ビット固定長の配列を作成する処理を紹介しながら、アルゴリズムや処理速度の評価と、基本的な APIの使い方を説明します。ところで、文字という用語には様々な解釈があり誤解を生じるので、以後は Javaの用語にならい、コードポイントと呼ぶことにします。従来の 16ビット配列で扱うものは char型もしくは char配列と呼びます。

例1-1

例1-1は、単純に文字列の先頭から16ビットのchar型に一つずつアクセスして、その値を 32ビットの配列に格納する処理です。サロゲートの有無を検査していないので、当然のことながら、サロゲートペアには対応していません。例1-2以後で紹介するアルゴリズムの処理速度を評価するためにベンチマークとして作成しました。

例1-1
int[] toCodePointArray(String str) {
    int len = str.length();   // the length of str
    int[] acp = new int[len]; // an array of code points

    for (int i = 0, j = 0; i < len; ++i) {
        acp[j++] = str.charAt(i);
    }
    return acp;
}

例1-1のソースコードは Java 1.4以前でもコンパイルできますが、以後のソースコードはすべてサロゲートAPIを使用しているため、Java 1.5以上でコンパイルすることを想定しています。また、ベンチマーク用のデータは次表にある環境で計測しました。

表1. ベンチマーク用環境
OS
Microsoft Windows XP Professional SP2
Java
IBM Java 1.5 SR7
CPU
Intel® Core™2 Duo CPU T8300 @ 2.40GHz
メモリ
2.97 GB RAM

例1-2

例1-2は、isSurrogatePair(), isHighSurrogete(), isLowSurrogate(), toCodePoint() のように、内部処理が単純な APIだけを使って作成しています。したがって、サロゲートを意識したアルゴリズムとしては比較的処理の速い部類のものとなります。処理時間は例1-1の 1.38倍です。しかし、ソースコードのステップ数は例1-1の約3倍あり、文字列へ順次アクセスを行うたびに、毎回、複雑な条件分岐と一時変数を多用するコードを書くのは、とても真のサロゲートプログラミングとは言えません。Shift-JIS対応アリゴリズムの悪夢がよみがえります。

例1-2
int[] toCodePointArray(String str) {
    int len = str.length();     // the length of str
    int[] acp;                  // an array of code points
    int surrogatePairCount = 0; // the count of surrogate pairs

    for (int i = 1; i < len; ++i) {
        if (Character.isSurrogatePair(str.charAt(i - 1), str.charAt(i))) {
            ++surrogatePairCount;
            ++i;
        }
    }
    acp = new int[len - surrogatePairCount];
    for (int i = 0, j = 0; i < len; ++i) {
        char ch0 = str.charAt(i);         // the current char
        if (Character.isHighSurrogate(ch0) && i + 1 < len) {
            char ch1 = str.charAt(i + 1); // the next char
            if (Character.isLowSurrogate(ch1)) {
                acp[j++] = Character.toCodePoint(ch0, ch1);
                ++i;
                continue;
            }
        }
        acp[j++] = ch0;
    }
    return acp;
}

例1-3

例1-2は一見複雑ですが、アルゴリズムをブロックごとに分けると、基本的に例1-1と同じ処理をおこなっています。

  • 文字列から配列に必要な長さ計算し、配列の領域を確保する。
  • 対象位置から、正しくコードポイント値(もしくはchar型値)を取得する。
  • 次の対象位置に移動する。

上記の 3個のブロックをそれぞれカプセル化したAPIが、codePointCount(), codePointAt(), offsetByCodePoints()です。それらを使用して記述したのが例1-3です。ソースコードの印象では、例1-1と同様に簡潔に記述できます。定型処理として使えます。ただし、処理時間は、例1-1の 2.80倍になっています。これは、offsetByCodePoints()が引数の境界条件を検査するために内部で時間を消費していることが原因です。それでも、各APIの拡張性や柔軟性、アルゴリズムの簡潔性を考慮すると、このアルゴリズムを定型処理の標準として採用すべきでしょう。

例1-3
int[] toCodePointArray(String str) {
    int len = str.length(); // the length of str
    int[] acp = new int[str.codePointCount(0, len)];

    for (int i = 0, j = 0; i < len; i = str.offsetByCodePoints(i, 1)) {
        acp[j++] = str.codePointAt(i);
    }
    return acp;
}
図2. 例1-3で使用した APIの相関図
図2. 例1-3で使用した APIの相関図
図2. 例1-3で使用した APIの相関図

例1-4

例1-4は、例1-3と同様にカプセル化した APIを使用していますが、配列に格納する順序が、文字列の末尾から先頭に向かっている点が異なります。処理時間は、基本的に例1-3とほぼ同じで、例1-1の 2.72倍を要します。対象位置の直前にあるコードポイントを取得する APIとして codePointBefore()を使用しています。offsetByCodePoints()は、第二引数に負数を指定することで、文字列の先頭方向へ向かって位置を取得できます。この APIを使う場合、境界条件の指定に注意が必要ですが、慣れてしまえば、例1-3と同様に定型処理として使えます。ループの脱出条件としての文字列長を管理する必要がないため、例1-3よりは、わずかですが処理が速くなるときもあるようです。

例1-4
int[] toCodePointArray(String str) {
    int len = str.length(); // the length of str
    int[] acp = new int[str.codePointCount(0, len)];
    int j = acp.length;     // an index for acp

    for (int i = len; i > 0; i = str.offsetByCodePoints(i, -1)) {
        acp[--j] = str.codePointBefore(i);
    }
    return acp;
}
図3. 例1-4で使用した APIの相関図
図3. 例1-4で使用した APIの相関図
図3. 例1-4で使用した APIの相関図

例1-5

例1-5は、例1-3で処理速度低下の原因になっていた offsetByCodePoints()の問題を解決したものです。通常、文字列を順次アクセスするときは、アクセスした場所のコードポイントも同時に取得しているので、その値を一時変数に格納し、charCount()に渡すことで、次のコードポイント開始位置が計算できます。charCount()の内部処理は offsetByCodePoint()と違い、きわめて単純なので、処理速度が大幅に向上します。この変更で、処理時間は例1-1の 1.68倍になり、複雑なアルゴリズムで記述した例1-2の処理時間に迫ります。高速で実用性のある定型処理です。

例1-5
int[] toCodePointArray(String str) {
    int len = str.length(); // the length of str
    int[] acp = new int[str.codePointCount(0, len)];
    int j = 0;              // an index for acp

    for (int i = 0, cp; i < len; i += Character.charCount(cp)) {
        cp = str.codePointAt(i);
        acp[j++] = cp;
    }
    return acp;
}
図4. 例1-5で使用した APIの相関図
図4. 例1-5で使用した APIの相関図
図4. 例1-5で使用した APIの相関図

例1-6

例1-6は文字列から 16ビットのchar配列を取り出し、その情報から 32ビットのコードポイント配列を作ります。Characterクラスの codePointCount(), codePointAt()は char配列にも対応しています。アルゴリズムも簡潔に表現でき、処理時間も、例1-1の 1.51倍まで短縮でき、非常に高速です。ただし、toCharArray() は呼び出されるたびに、新規の配列を作り、値をコピーしているので、ソースコードの印象が簡潔だからといって、toCharArray()の特性を理解せずに頻繁に呼び出すと、思わぬオーバーヘッドを生み出します。例1-6は、toCharArray()の特性を理解したうえで、処理速度の向上を図るときに採用するアルゴリズムです。

例1-6
int[] toCodePointArray(String str) {
    char[] ach = str.toCharArray(); // a char array copied from str
    int len = ach.length;           // the length of ach
    int[] acp = new int[Character.codePointCount(ach, 0, len)];
    int j = 0;                      // an index for acp

    for (int i = 0, cp; i < len; i += Character.charCount(cp)) {
        cp = Character.codePointAt(ach, i);
        acp[j++] = cp;
    }
    return acp;
}

例1-7

例1-7は、今までのアルゴリズムとは趣旨を変えています。Java言語はオブジェクト指向プログラミング言語なのに、一々、インデックスで文字列の位置を管理するのは美しくないという意見があります。もっともです。ここでは、java.nio.CharBufと java.nio.IntBufを使った処理を紹介します。CharBufは対象文字列をラップし効率良くアクセスするためのインターフェースです。IntBufは、32ビット配列にインデックスを意識せずにアクセスするためのインターフェースです。ここで紹介した例では、Characterクラスが提供する codePointCount()と codePointAt()を使用し、コードポイント数とコードポイント値を取得しています。CharBufは CharSequenceインターフェースを実装しているので、これらの APIの引数として使用可能です。同様に CharSequenceインターフェースを実装しているクラスは String, StringBuffer, StringBuilderです。

例1-7の処理時間は例1-1の 1.81倍です。残念ながら、java.nio.Buffer処理の基本APIである get(), put()の組み合わせを使っていません。get()はサロゲートペアを認識しないので、代わりに codePointAt()を使っています。ただし、この APIには、get()、put()が持つバッファ内部の位置を自動的に移動する機能はなく、position()で外部から位置を移動しています。これは本来の java.nio.CharBufが目指すオブジェクト指向的な使用方法とは思えません。この例は参考情報として紹介しました。

例1-7
int[] toCodePointArray(String str) {
    CharBuffer cBuf = CharBuffer.wrap(str); // Buffer to wrap str
    IntBuffer iBuf = IntBuffer.             // Buffer to store code points
            allocate(Character.codePointCount(cBuf, 0, cBuf.capacity()));

    if (cBuf.capacity() > 0) {
        int cp; // the current code point
        do {
            cp = Character.codePointAt(cBuf, 0);
            iBuf.put(cp);
        } while (cBuf.position(cBuf.position() + Character.charCount(cp)).
                remaining() > 0);
    }
    return iBuf.array();
}

例1-1から例1-7までの処理時間とまとめ

以下の表は、例1-1から例1-7までのアルゴリズムを使い、サロゲートペアを含んだ char型3万個の文字列を対象にして、コードポイント配列を1万回作成したときの処理時間を一覧表で紹介しています。CPUの速度、メモリの使用状況、バックグラウンドタスクの状況により所要時間の絶対値は変わるので、相対値に注目してください。

表2. 例1-1から例1-7までの処理時間とまとめ
処理時間例1-1比まとめ
1-12031ms1.00 (サロゲートペア非対応)
1-22797ms1.38 [非推奨]
char値を直接判定するAPIで作成しています。高速な処理が可能ですが、サロゲートペアを認識するためには複雑な条件式が必要でソースコードの保守性が問題です。基本的に非推奨です。高速処理が必要な場合は例1-3をベースにして高速化を検討します。
1-35687ms2.80 [定型処理]
文字列単位でサロゲートペアを認識するAPIで作成しています。ソースコードが簡潔に記述でき保守性に優れます。処理時間は例1-1の 2倍以上ですが、サロゲートペアの定型処理は、このアルゴリズムから始めるべきです。高速化が必要な場合は、例1-5、例1-6を検討します。
1-45516ms2.72 [定型処理の応用]
例1-3で使用した codePointAt()の代わりに、codePointBefore()を使用し、文字列の末尾から前方への順次アクセスを実現しています。その他の特徴は例1-3と同様です。
1-53406ms1.68 [定型処理の高速化]
例1-3の offsetByCodePoints()を charCount()に置き換えることで高速化を実現します。
1-63062ms1.51 [定型処理の高速化]
例1-5の高速化に加えて、Stringクラスからchar配列を生成して、配列を操作することで高速化を実現します。
1-73672ms1.81 [参考]
java.nio.Bufferクラスと CharSequenceインターフェースによるサロゲートペアの処理例です。
表3. 例1-2から例1-7で紹介したサロゲートAPI
クラスメソッド/コンストラクタ操作対象
1-2Characterstatic boolean isSurrogatePair(char high, char low)char型
static boolean isHighSurrogate(char ch)
static boolean isLowSurrogate(char ch)
static int toCodePoint(char high, char low)
1-3Stringint codePointCount(int begin, int end)Stringクラス
int offsetByCodePoints(int index, int cpOffset)
int codePointAt(int index)
1-4Stringint codePointBefore(int index)Stringクラス
1-5Characterstatic int charCount(int cp)コードポイント
1-6Characterstatic int codePointCount(char[] ach, int offset, int count)char配列
static int codePointAt(char[] ach, int index)
1-7Characterstatic int codePointCount(CharSequence seq, int begin, int end)CharSequenceインターフェース
static int codePointAt(CharSequence seq, int index)

ランダムアクセス

この節では、文字列内にある任意のコードポイントにランダムアクセスするときの処理時間を評価します。Stringクラスは内部にコードポイント位置に関連する情報を保持しません。したがって、コードポイント位置をランダムに取得するには、内部にある基底位置から、毎回、オフセットを計算する処理が発生します。この仕様により、基底位置から現在位置までの間隔が離れているときは、非常に計算時間が掛かります。この性質は、サロゲートAPIを使用する場合、最も慎重になる部分です。

ここでランダムアクセスの応用例として簡易変換フィルターを考えてみます。例2-1は、全角数字を半角数字に変換する処理です。全角数字の文字列と半角数字の文字列を用意し、それぞれ同じ順番で対応する全角数字と半角数字を並べます。対象文字列のなかに全角数字が見つかった場合、全角数字文字列に位置を問い合わせ、それと同じ位置にある半角数字を半角数字文字列から取り出して、全角から半角への変換を行います。大規模システムには向かないものの、ソースの可読性がよく、Java言語と類似の文法を持つ JavaScriptでは、Webブラウザーで入力文字を置き換えるときに使われます。ただし、このアルゴリズムが有効なのは、双方がサロゲートペアを含んでいないときに限ります。サロゲートペアが含まれると、文字列長とコードポイント数が異なるので、単純な処理では対応位置を見つけられなくなります。

例2-1 ランダムアクセスの応用例
String toHalfWidthNumber(String str) {
    String fullWidth = "0123456789";
    String halfWidth = "0123456789";
    StringBuilder sb = new StringBuilder(str);

    for (int i = 0, len = sb.length(); i < len; ++i) {
        int pos = fullWidth.indexOf(sb.charAt(i));
        if (pos >= 0) {
            sb.setCharAt(i, halfWidth.charAt(pos)); // random access
        }
    }
    return sb.toString();
}

例2-2

さて、ここで文字列1の任意のコードポイントと対応関係にある文字列2のコードポイント位置を抜き出す処理を考えます。例2-1で紹介したテクニックは使えません。サロゲートペアが含まれている文字列では、対応関係にあるコードポイント位置が文字列1と文字列2とで一致しない場合があるからです。

例2-2 は基本的な処理です。まず、文字列1の先頭から現在位置までのコードポイント数を codePointCount()で取得し、次に、その値を、文字列2の offsetByCodePoints()に渡し、文字列2の対応位置を求めています。しかし、文字列の長さに比例して処理時間が掛かり、任意の位置を対象とするループ処理には向いていません。他の例と比較するために、平均処理時間の計算式を紹介します。

  • 平均処理時間関数: f(x) = ∫xdx
  • 平均処理時間: f(1.0)-f(0.0) = 0.5
  • 変数 x は文字列内部の位置、x = 0.0 は文字列の先頭、x = 1.0 は文字列の末尾を意味する。以後の例でも同様
例2-2
int parallelIndexOf(String str1, int pos1, String str2) {
    return str2.offsetByCodePoints(0, str1.codePointCount(0, pos1));
}
図5. 先頭を基底とした APIの相関図 (▲▼は現在位置を示す)
図5. 先頭を基底とした APIの相関図(▲▼は現在位置を示す)
図5. 先頭を基底とした APIの相関図(▲▼は現在位置を示す)

例2-3

例2-2では、現在位置までのコードポイント数を取得するために、基底位置を文字列の先頭に設定しましたが、例2-3では、先頭に加えて末尾も基底位置の候補にしています。現在位置から、先頭と末尾の距離を比較して、近いものを基底位置として選びます。基底位置が末尾になったときは、offsetByCodePoints()の第二引数は負数になります。この変更により、例2-2と比べ、平均処理時間は半分になります。ただし、これ以上の高速化効果はなく、同様に任意の位置を対象とするループ処理には向いていません。

  • 平均処理時間関数: f(x) = ∫xdx (0≦x≦0.5), g(x) = ∫(1-x)dx (0.5≦x≦1.0)
  • 平均処理時間: f(0.5)-f(0.0)+g(1.0)-g(0.5) = 0.25
例2-3
int parallelIndexOf(String str1, int pos1, String str2) {
    int len1 = str1.length(); // the length of str1
    int len2 = str2.length(); // the length of str2

    return (pos1 < len1 / 2)
        ? str2.offsetByCodePoints(0,     str1.codePointCount(0, pos1))
        : str2.offsetByCodePoints(len2, -str1.codePointCount(pos1, len1));
}
図6. 末尾を基底とした APIの相関図 (▲▼は現在位置を示す)
図6. 末尾を基底とした APIの相関図(▲▼は現在位置を示す)
図6. 末尾を基底とした APIの相関図(▲▼は現在位置を示す)

例2-2b

例2-2、例2-3では現在位置の情報しかありませんでしたが、例2-2bでは前回位置の情報も活用します。ランダムアクセスと言っても、完全にランダムにアクセスする状況だけでなく、現在位置と前回位置が近接する状況も多くあります。この場合、アルゴリズムに少しの工夫を施すことで、ループ処理の大幅な速度向上が見込めます。また、ループ処理で前回位置を保存することは、ソースコードの変更も複雑になりません。(一方、前々回以前の位置を保存することスタック管理が必要になり、大幅な設計変更が必要になります。)

まず、現在位置から見て、文字列の先頭と前回位置までの距離を比較し、近いものを基底位置として選びます。基底位置と現在位置の大小を比較して、基底位置が大きい場合は、offsetByCodePoints()の第二引数は負数になります。次に、完全にランダムアクセスするときの計算式を示します。平均処理時間は例2-2の 3分の 2です。ランダムアクセスの平均処理時間では例2-3より劣りますが、ループ内部で近接位置への移動が多く発生する状況では、大幅な速度向上が見込めます。

  • 平均処理時間関数: f(x) = ∫xdx-∫x2dx (0≦x≦0.5), g(x) = ∫xdx-∫x2dx+∫2(x-0.5)2dx (0.5≦x≦1.0)
  • 平均処理時間: f(0.5)-f(0.0)+g(1.0)-g(0.5) ≒ 0.333
例2-2b
int parallelIndexOf(String str1, int pos1, int pre1, String str2, int pre2) {
    int base1 = (pos1 < pre1 / 2) ? 0 : pre1; // a base position for str1
    int base2 = (pos1 < pre1 / 2) ? 0 : pre2; // a base position for str2

    return str2.offsetByCodePoints(base2, (base1 < pos1)
            ?  str1.codePointCount(base1, pos1)
            : -str1.codePointCount(pos1, base1)); 
}
図7. 前回位置を基底とした APIの相関図 (▲▼は現在位置、▽△は前回位置を示す)
図7. 前回位置を基底とした APIの相関図(▲▼は現在位置、▽△は前回位置を示す)
図7. 前回位置を基底とした APIの相関図(▲▼は現在位置、▽△は前回位置を示す)

例2-3b

例2-3bは、前回位置の情報を例2-3に適用したものです。現在位置から、文字列の先頭、末尾、前回位置の 3点に対して、もっとも距離が近いものを基底位置として採用し、現在位置を計算する方法です。平均処理時間は例2-2の 3分の 1まで短縮され、アクセス位置が局所化されたときは、さらに速度向上が期待できます。ただし、ソースコードの変更を最小限にとどめてサロゲートAPIを活用するという、この記事の趣旨では多少複雑すぎる気もします。例2-2b程度の高速化が簡潔な処理として限界かもしれません。

  • 平均処理時間関数: f(x) = ∫xdx-∫x2dx (0≦x≦0.5), g(x) = ∫(1-x)dx-∫(1-x)2dx (0.5≦x≦1.0)
  • 平均処理時間: f(0.5)-f(0.0)+g(1.0)-g(0.5) ≒ 0.1667
例2-3b
int parallelIndexOf(String str1, int pos1, int pre1, String str2, int pre2) {
    int len1 = str1.length(); // the length of str1
    int len2 = str2.length(); // the length of str2
    int base1 = (pos1 < pre1 / 2) ? 0 : (pos1 < (pre1 + len1) / 2) ? pre1 : len1;
    int base2 = (pos1 < pre1 / 2) ? 0 : (pos1 < (pre1 + len1) / 2) ? pre2 : len2;

    return str2.offsetByCodePoints(base2, (base1 < pos1)
            ?  str1.codePointCount(base1, pos1)
            : -str1.codePointCount(pos1, base1));
}

平均処理時間の比較

以下の表は、この節で紹介した各例の処理時間を比較したものです。適度な長さの文字列を作成し、擬似完全乱数列と擬似部分乱数列を作って、その文字列の全コードポイントにアクセスしました。この場合も、処理時間は、CPU速度、メモリの使用状況、バックグラウンドタスクの状況によって変化しますので、絶対値よりも相対値に注目してください。また評価に使った乱数列は、完全な乱数列ではないので、各例の時間比は厳密な理論値と違います。擬似部分乱数列の場合、平均処理時間の大幅な短縮が確認できます。

  • 擬似完全乱数列(例0から99までの場合): 0, 99, 2, 97, 4, 95, ..., 98, 1
  • 擬似部分乱数列(例0から99までの場合): 0, 1, 2, ..., 9, 99, 98, 97, ..., 90, 20, 21, 22, ..., 29, 79, 78, 77, ..., 70, 40, 41, 42, ..., 89, 19, 18, 17, ..., 10
表4. 平均処理時間の比較
基底位置擬似乱数列文字列長
1200024000360004800060000
2-2先頭完全313ms968ms2468ms4406ms6890ms
2-3先頭、末尾完全125ms453ms984ms1828ms2875ms
2-2b先頭、前回完全297ms610ms1407ms2482ms3875ms
2-3b先頭、前回、末尾完全78ms266ms750ms1156ms1812ms
2-2b先頭、前回部分62ms172ms313ms375ms594ms
2-3b先頭、前回、末尾部分16ms47ms93ms234ms281ms

情報取得API

この節では、コードポイントを引数に取る情報取得APIの紹介をします。前節までとは違い、アルゴリズムの紹介や処理時間の測定はしませんので、気楽に目を通してください。大半の APIがサロゲート対応以前は、char型の値を引数に取る情報取得APIとして実装されていました。したがって、引数を int型に変更するだけで、サロゲート対応APIが使用可能になります。以下の表で APIの一覧を示し、U+53F1(叱)と U+20B9F(叱)を引数に取るときの値を紹介します。また、U+20B9Fをサロゲートペアで表現すると、U+D842 U+DF9F になります。ハイサロゲート部分 U+D842の値を参考情報として紹介します。サロゲートペア対応の有無で取得する値が変化します。

表5. 情報を取得するサロゲートAPI
クラスメソッド/コンストラクタ値(U+53F1)値(U+20B9F)値(U+D842)
Characterstatic int charCount(int cp)121
static byte getDirectionality(int cp)000
static int getNumericValue(int cp)-1-1-1
static int getType(int cp)5519
static boolean isDefined(int cp)truetruetrue
static boolean isDigit(int cp)falsefalsefalse
static boolean isISOControl(int cp)falsefalsefalse
static boolean isIdentifierIgnorable(int cp)falsefalsefalse
static boolean isJavaIdentifierPart(int cp)truetruefalse
static boolean isJavaIdentifierStart(int cp)truetruefalse
static boolean isLetter(int cp)truetruefalse
static boolean isLetterOrDigit(int cp)truetruefalse
static boolean isLowerCase(int cp)falsefalsefalse
static boolean isMirrored(int cp)falsefalsefalse
static boolean isSpaceChar(int cp)falsefalsefalse
static boolean isSupplementaryCodePoint(int cp)falsetruefalse
static boolean isTitleCase(int cp)falsefalsefalse
static boolean isUnicodeIdentifierPart(int cp)truetruefalse
static boolean isUnicodeIdentifierStart(int cp)truetruefalse
static boolean isUpperCase(int cp)falsefalsefalse
static boolean isValidCodePoint(int cp)truetruetrue
static boolean isWhitespace(int cp)falsefalsefalse
static char[] toChars(int cp){'\u53f1'}{'\ud842','\udf9f'}{'\ud842'}
static int toLowerCase(int cp)(不変)(不変)(不変)
static int toTitleCase(int cp)(不変)(不変)(不変)
static int toUpperCase(int cp)(不変)(不変)(不変)
Fontboolean canDisplay(int cp)(フォント依存)(フォント依存)(フォント依存)
FontMetricsint charWidth(int cp)(FontMetrics依存)(FontMetrics依存)(FontMetrics依存)
Stringint indexOf(int cp)(文字列依存)(文字列依存)(文字列依存)
int lastIndexOf(int cp)(文字列依存)(文字列依存)(文字列依存)

その他のAPI

これまでの節で、通常使用されるサロゲートAPIの説明は終わりましたが、この節では残りのサロゲートAPIをすべて紹介します。ほとんどの APIが、前節までに紹介された APIに詳細設定用の引数を追加したもので、基本的なコンセプトは既に説明済みです。その他に、コードポイント用の例外として IllegalFormatCodePointExceptionが提供されています。最後に、まだ紹介してない APIである Stringクラスのコンストラクタと StringBuffer, StringBuilderの appendCodePoint()を使用したソースコードを紹介します。

コードポイントから文字列を生成

コードポイント配列から文字列の生成例
int[] acp = {'\u53f1', 0x20b9f};
String str1 = new String(acp, 0, acp.length);
String str2 = new StringBuilder().
        appendCodePoint(acp[0]).
        appendCodePoint(acp[1]).toString();

コードポイント配列から文字列を生成するのは非常に簡単です。Stringクラスは、コンストラクタでコードポイント配列を受け取ることができ、直接 文字列を生成します。前節までに説明してきた、文字列からコードポイント配列を生成する間接的な手法とは対照的です。次に、StringBuffer, StringBuilderの appendCodePoint()も、コードポイントから文字列を生成する APIとして使えます。特に、大量の計算を要するループ処理内部では、appendCodePoint()が活躍します。

表6. その他のサロゲートAPI
クラスメソッド/コンストラクタコメント
Characterstatic int codePointAt(char[] ach, int index, int limit)char配列用codePointAt()の詳細設定版
static int codePointBefore(char[] ach, int index)char配列用codePointBefore()
static int codePointBefore(char[] ach, int index, int start)char配列用codePointBefore()の詳細設定版
static int codePointBefore(CharSequence seq, int index)CharSequence用codePointBefore()
static int digit(int cp, int radix)getNumericValue()の詳細設定版
static int offsetByCodePoints(char[] ach, int start, int count, int index, int cpOffset)char配列用offsetByCodePoints()
static int offsetByCodePoints(CharSequence seq, int index, int cpOffset)CharSequence用offsetByCodePoints()
static int toChars(int cp, char[] dst, int dstIndex)呼び出し側で配列を管理する toChars()
StringString(int[] acp, int offset, int count)コードポイント配列から文字列を生成する
int indexOf(int cp, int fromIndex)indexOf()の詳細設定版
int lastIndexOf(int cp, int fromIndex)lastIndexOf()の詳細設定版
StringBufferStringBuffer appendCodePoint(int cp)コードポイントを追加する
int codePointAt(int index)StringBuffer用codePointAt()
int codePointBefore(int index)StringBuffer用codePointBefore()
int codePointCount(int beginIndex, int endIndex)StringBuffer用codePointCount()
int offsetByCodePoints(int index, int cpOffset)StringBuffer用offsetByCodePoints()
StringBuilderStringBuilder appendCodePoint(int cp)コードポイントを追加する
int codePointAt(int index)StringBuilder用codePointAt()
int codePointBefore(int index)StringBuilder用codePointBefore()
int codePointCount(int beginIndex, int endIndex)StringBuilder用codePointCount()
int offsetByCodePoints(int index, int cpOffset)StringBuilder用offsetByCodePoints()
IllegalFormatCodePointExceptionIllegalFormatCodePointException(int cp)不正コードポイント例外を生成する
int getCodePoint()コードポイント値を取得する

まとめ

この記事では、サロゲートAPIの基本的な使用法と Java 1.5でサポートされた全APIを紹介しました。この記事を読む前は、サロゲート対応には何か大掛かりな対策が必要だと考えられた方も多かったのではないでしょうか。サロゲート対応は、従来の文字列処理アルゴリズムに対して、局所的に専用APIへ置き換えることで解決する場合がほとんどです。また、処理時間も工夫を施すことで 1.5倍増えるだけです。プログラム全体に対する文字列処理時間の割合を考えれば、全体の処理時間に大きな影響はないと思われます。一方、サロゲートペアに対応することは、ソースコードの安定性、拡張性、保守性の点から大きなメリットがあります。これらのメリットは、プログラム作成者にとって、サロゲート対応を促進するモチベーションになります。サロゲート未対応のソフトウェアは、日本の JIS X 0213や 中国の GB18030のデータを完全に扱えず、障害発生時に一時しのぎの対応を繰り返すと、ソースコードの管理が困難になっていきます。開発の初期段階からサロゲートペアを意識したプログラミングを心がけていくべきでしょう。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=472115
ArticleTitle=Java による Unicode サロゲートプログラミング
publish-date=03192010