Javaの正規表現で下位サロゲートコードにマッチするパターンを書くと、完全なサロゲートペアの後半16ビットにマッチしてしまうことがあります。少なくとも、次のバージョンはそのような挙動を示します。
- Oracle Hotspot 1.8.0_72
- OpenJDK 1.8.0_66
詳細
次のパターンは、問題を起こす正規表現をJavaの文字列リテラルで表したものです。それぞれ、単一の下位サロゲートU+DC00
あるいは下位サロゲート全体の文字クラス (U+DC00
-U+DFFF
) を表しています。
- "\\udc00"
- "\\x{dc00}"
- "[\\udc00-\\udfff]"
- "[\\x{dc00}-\\x{dfff}]"
- "[\\p{blk=Low Surrogates}]"
この挙動は、最後のパターンを使って、孤立した下位サロゲートを検出するプログラムを書いている時に発見しました。孤立した下位サロゲートとは、上位サロゲートコード (U+D800
-U+DBFF
) に後続していない下位サロゲートのことです。UTF-16のエンコードにおいて、孤立したサロゲートコードは “ill-formed” と定義されていますが、Javaの文字列値はこのようなシーケンスを含む可能性があります。たとえば">>\udc00<<"
のように。
上記のパターンは期待通りに孤立したサロゲートコードにマッチしましたが、完全なサロゲートペアの後半16ビットにもマッチしてしまいました。たとえば、コードポイントU+010000
を表す"\ud800\udc00"
の、2つめのcharにマッチしてしまいます。
Pattern regex = Pattern.compile("[\\p{blk=Low Surrogates}]"); Matcher matcher = regex.matcher("\ud800\udc00"); // U+010000 System.out.println(matcher.find()); // => true System.out.println(matcher.start()); // => 1 System.out.println(matcher.end()); // => 2
この挙動は、Unicode Technical Standard #18 の “RL1.7 Supplementary Code Points”に違反しているように見えます。ここでは次のように規定されています。
UTF-16を使う場合、先行するサロゲートと後続するサロゲートのペアからなるシーケンスは、マッチにおいて単一のコードポイントとして扱わなければならない。
回避策
生のサロゲートコードをPattern#compileに渡して生成したパターンは、孤立したサロゲートコードのみにマッチし、完全なサロゲートペアの一部にはマッチしません。
Pattern p1 = Pattern.compile("\udc00"); System.out.println(p1.matcher("\ud800\udc00").find()); System.out.println(p1.matcher(">>\udc00<<").find()); // => false true Pattern p2 = Pattern.compile("[\udc00-\udfff]"); System.out.println(p2.matcher("\ud800\udc00").find()); System.out.println(p2.matcher(">>\udc00<<").find()); // => false true
*1:2月10日8:30追記。