こんにちは。Nemoです。
今回、Javaで正規表現による文字列置換を行う際、String java.lang.String.replaceAll()を使った置換を実装を行いました。その時の云々を。
まずは、replaceAllメソッドを使った実装例です。
String str = “★$01234$★$98765$★”; str = str.replaceAll(“\\$(\\d{5})\\$”, “Hello!”); System.out.println(str); |
★Hello!★Hello!★ |
という単純な実装ですが、このreplaceAllメソッドを利用した置換の場合、期待する結果が得られない場合があります。
String str = “★$01234$★$98765$★”; str = str.replaceAll(“\\$(\\d{5})\\$”, “$1”); System.out.println(str); |
置換結果が★$1★$1★になると思われますが、実際には
★01234★98765★ |
という結果になります。また次のパターンの場合、
String str = “★$01234$★$98765$★”; str = str.replaceAll(“\\$(\\d{5})\\$”, “\\”); System.out.println(str); |
java.lang.StringIndexOutOfBoundsExceptionが発生してしまいます。
これはreplaceAllメソッド内で、java.util.regex.MatcherクラスのreplaceAllメソッドを呼んでいます。
その中で\\はエスケープ文字、$は正規表現グループを参照しようとする為です。(実際はjava.util.regex.MatcherクラスのappendReplacementメソッド内で判定を行なっています。)
これだと「$」を含む文字列に置換したい場合などにはちょっと使えません。
少し話はそれますが、String java.lang.Stringクラスでは他にreplaceメソッドがあります。
もちろん、このreplaceメソッドは正規表現による置換は行いません。ですので、
String str = “★$01234$★$98765$★”; str = str.replace(“\\$(\\d{5})\\$”, “$1”); System.out.println(str); |
の結果は、
★$01234$★$98765$★ |
です。
このreplaceメソッド自身も実際は、replaceAllメソッドと同様に、java.util.regex.MatcherクラスのreplaceAllメソッドを呼んでいます。
String java.lang.String.replace
public String replace(CharSequence target, CharSequence replacement) {return Pattern.compile(target.toString(),Pattern.LITERAL).matcher(this).replaceAll(Matcher.quoteReplacement(replacement.toString()));} |
しかし、replaceメソッドでは検索する文字列を、java.util.regex.Patternクラスのcompileを実施する際にリテラルで指定することで、正規表現による検索を回避し、また置換する文字列を、java.util.regex.MatcherクラスのquoteReplacementメソッドを通すことで、\\と$をエスケープしています。
このことから、replaceメソッドでは正規表現による置換を行なっていないため、置換したいパターンが複数ある場合には
String str = “★$01234$★$98765$★”; str = str.replace(“$01234$”, “$1”); str = str.replace(“$98765$”, “$1”); System.out.println(str); |
と、複数の置換処理を行う必要があります。
不便ですね。
ということは、replaceAllメソッドとreplaceメソッドを組み合わせることで、正規表現による置換が可能です。(java.util.regex.MatcherクラスのquoteReplacementメソッドを通した置換文字列を、String java.lang.String.replaceAllメソッドの置換文字列に渡す)
String str = “★$01234$★$98765$★”; str = str.replaceAll(“\\$(\\d{5})\\$”, Matcher.quoteReplacement(“$1”)); System.out.println(str); |
この置換を用いた場合、replaceメソッドを複数実施するパターンと比べても、十分なパフォーマンスが期待されます。以下が検証した結果です。
検証に利用する文字列は以下の20,000行の文字列です。
00001 : ABCDEFGHIJKLMNOPQRSTUVWXYZ$00000$abcdefghijklmnopqrstuvwxyz・・・00100 : ABCDEFGHIJKLMNOPQRSTUVWXYZ$00099$abcdefghijklmnopqrstuvwxyz 00101 : ABCDEFGHIJKLMNOPQRSTUVWXYZ$00000$abcdefghijklmnopqrstuvwxyz・・・20000 : BCDEFGHIJKLMNOPQRSTUVWXYZ$00099$abcdefghijklmnopqrstuvwxyz |
置換処理内容は、「$で囲まれた5桁の数値を”Hello!”に置換」としましょう。
検証ロジック1(単純な正規表現での検索+置換ロジック)
String str = ※検証に利用する文字列※ String regex = “\\$(\\d{5})\\$”; String replacement = “Hello!”; long start = System.currentTimeMillis(); Matcher m = Pattern.compile(regex).matcher(str); List<String> targets = new ArrayList<String>(); while (m.find()) {if (targets.contains(m.group(0))) {continue;}targets.add(m.group(0));} for (String s : targets) {str = str.replace(s, replacement);} long stop = System.currentTimeMillis(); System.out.println(String.format(“—[%dms]”, (stop – start)); |
検証ロジック2(replaceAllメソッドとreplaceメソッドの組み合わせ)
String str = ※検証に利用する文字列※ String regex = “\\$(\\d{5})\\$”; String replacement = “Hello!”; long start = System.currentTimeMillis(); String rt = str.replaceAll(regex, Matcher.quoteReplacement(replacement)); long stop = System.currentTimeMillis(); System.out.println(String.format(“—[%dms]”, (stop – start)); |
測定結果(ms)
10回測定した平均 | |
検証ロジック1 | 13,619ms |
検証ロジック2 | 263.9ms |
歴然ですね。
折角なので、プロファイル結果を見てみました。
検証ロジック1では、最もCPUを使用しているメソッド(約40%強)は「java.lang.AbstractStringBuilder.append」。詳細を見てみると、呼び出し元は「java.util.regex.Matcher.replaceAll」でした。
TRACE 300279:java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:482)java.lang.StringBuffer.append(StringBuffer.java:309)java.util.regex.Matcher.appendReplacement(Matcher.java:838)java.util.regex.Matcher.replaceAll(Matcher.java:905)・・・ rank self accum count trace method 1 42.38% 42.38% 353 300279 java.lang.AbstractStringBuilder.append |
また、検証ロジック2のプロファイル結果はどうだったかというと、こちらも最もCPUを使用しているメソッド(約10%)も「java.util.regex.Matcher.replaceAll」でした。
TRACE 300262:java.util.regex.Matcher.replaceAll(Matcher.java:906)java.lang.String.replaceAll(String.java:2210)・・・ rank self accum count trace method 1 9.09% 9.09% 5 300262 java.util.regex.Matcher.replaceAll |
パフォーマンスの結果に差異はありますが、この結果から、どちらのロジックでもjava.util.regex.Matcher.replaceAllがボトルネックになっていることがわかります。
参考までに、java.util.regex.Matcher.replaceAllを利用しないパターンの実装です。
検証ロジック3
String str = ※検証に利用する文字列※ String regex = “\\$(\\d{5})\\$”; String replacement = “Hello!”; long start = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); Matcher m = Pattern.compile(regex).matcher(str); int s = 0; while (m.find()) {sb.append(str.substring(s, m.start()));sb.append(replacement);s = m.end();} if (s < str.length()) {sb.append(str.substring(s, str.length()));} long stop = System.currentTimeMillis(); System.out.println(String.format(“—[%dms]”, (stop – start)); |
測定結果(ms)
10回測定した平均 | |
検証ロジック1 | 13,619ms |
検証ロジック2 | 263.9ms |
検証ロジック3 | 229.8ms |
というように、java.util.regex.Matcher.replaceAllを利用しない場合、幾らかですがパフォーマンスの改善が見られました。
まとめ
・String java.lang.String.replaceAllはjava.util.regex.Matcher.quoteReplacementを組み合わせることで正規表現を使った置換が可能。
・java.util.regex.Matcher.replaceAllメソッドを使わない置換では、多少のパフォーマンス改善が見られる。
またね。