2008年5月30日金曜日

String#offsetByCodePointsでハマリ

0 コメント


文字列の補助文字対応をしていてハマッタのでメモ。


症状


下記のようにUnicodeのコードポイント単位で処理をしたいが、


offsetByCodePointsの値に期待値がこないで、妙にでかい値がくる。なぜ?!



for (int i = 0; i < s.length(); i = s.offsetByCodePoints(i, 1)) {
...
}


調査



下記のようなテストプログラムでoffsetByCodePointsの出力を調べる。


期待値は1.



class Test {
public static void main(String[] args) throws Exception {
String org = "*****ハロー";
String word = org.substring(5, org.length());
p("offsetByCodePoints:%s\n", word.offsetByCodePoints(0, 1));
p("%s\n", word);
}

public static void p(String format, Object... args)
{
System.out.printf(format, args);
}
}



しかし、出力は以下となる。



$ java Test
offsetByCodePoints: 6
ハロー


この6という数字はなにか。


JavaDocによると、String#substringは新しいStringのインスタンスを返すと記述されている。


しかし実はsubustringで生成されたインスタンスは、完全に新しく生成されるわけではないのだ。


Stringのインスタンスは内部に文字列を保持しているが、メモリ効率をよくするために、


同じ文字列をnew Stringした場合は、同じ文字列をメモリ上指し示すような作りになっている。


subustringメソッドを実効した場合も同様で、subustringを実効したStringインスタンスと


同じ文字列を指すのだ(内部にオフセット値をもっていて、この値を変えているだけ。Stringクラスのソースを読むと分かる)



このことから以下のようなループ処理をするときは、substringを使用してはいけない。



for (int i = 0; i < s.length(); i = s.offsetByCodePoints(i, 1)) {
...
}


使用する場合は、



String word = new String(org.substring(5, org.length()));


としなくてはならい。



結論


substringで生成された文字列に対して、offsetByCodePointsを使用してはならない。


じゃあ、どうやって文字ループを実現するかだが、以下のようにした方がいいかもしれない。



for (int i = 0; i < s.length(); ) {
int c = s.codePointAt(i);
p("%x\n", c);
i += Character.charCount(c);
}


offsetByCodePointsはループに使用するなってことだね。



今回使用したテストプログラム



import java.io.*;

class Test {
public static void main(String[] args) throws Exception {
String org = "*****ハロー";
String word = org.substring(5, org.length());
p("offsetByCodePoints:%s\n", word.offsetByCodePoints(0, 1));
p("%s\n", word);
testLoop1(word);
testLoop2(word);
}

public static void p(String format, Object... args)
{
System.out.printf(format, args);
}

public static void testLoop1(String s) {
p("===================\n");
p("offsetByCodePoints\n");
for (int i = 0; i < s.length(); i = s.offsetByCodePoints(i, 1)) {
int c = s.codePointAt(i);
p("%x\n", c);
}
}

public static void testLoop2(String s) {
p("===================\n");
p("charCount\n");
for (int i = 0; i < s.length(); ) {
int c = s.codePointAt(i);
p("%x\n", c);
i += Character.charCount(c);
}
}
}


追記


この症状はバグとして報告されていて、Java6で実行したら問題なかったよ。。。


http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6242664