Javaの文字列結合で+の方がStringBuilderより速かったりもする
※コメントでご指摘いただきましたが、コンパイラによる最適化には適用されるケースと適用されないケースがあります。
全然昔の知識ではないです。サンプルがたまたま最適化されるケースだっただけです。最適化が適用されるのはループ内とか一部だけです。ループ外にString変数を用意して、この変数に文字列結合すれば悲惨なことになります。
こちらの記事を読んで誤解して、StringBuilderを使わず被害を受けた人を何人か見かけたので、できれば下記URLなどを見て訂正をしてもらいたいところです。
http://www.pellegrino.link/2015/08/22/string-concatenation-with-java-8.html
この点において、まるですべての状況において+結合は問題ない、と受け取られるように書いてしまっていました。迷惑をおかけした方々には深くお詫び申し上げます。
執筆時点ではコード例のように同一スコープ内のループで文字列を組み立てる、という状況しか想定しかしていませんでしたが、明らかに考慮不足でした。大変失礼いたしました。
================================
昨日、たまたま下のページを見つけました。
Javaでは文字列の連結に+演算子を使用するのを控えましょう<
どういうサイトなのかはよく知らないのですが、+演算子による文字列結合は非効率だから、StringBufferもといStringBuilderを使いましょうという記事です。
あー、そんな話もあったなーとか思ったのですが、よく見ると最終更新日が2013年4月とつい最近。ちょっといただけない感じですね。
別に知らなくてもそんなに困らないと思いますが、2013年のこの時代に「+演算子の方が遅いから!」なんてどや顔で講釈たれると残念な目に遭います。実際に試してみましょう。
Javaは現時点で最新の1.7.0u21を使用します。
※バージョン、環境によって結果は左右します
ソースコードも上記ページから拝借します。
サンプルは1億回のループの中で文字列結合を試すものです。まず、駄目な例として紹介されている+演算子結合。
①+演算子記号
・ソースコード
public class Test { public static void main(String[] args) { long startedTime = System.currentTimeMillis(); String message = null; String strValue1 = "value1"; String strValue2 = "value2"; String strValue3 = "value3"; String strValue4 = "value4"; String strValue5 = "value5"; for(int i = 0; i < 100000000; i++) { message = strValue1 + strValue2; message = message + strValue3; message = message + strValue4; message = message + strValue5; } long endedTime = System.currentTimeMillis(); System.out.println("所要時間(ms):" + (endedTime - startedTime)); } }
・実行結果
所要時間(ms):4583
結果は約4.6秒ですね。
次です。
②StringBuilder
・ソースコード
public class Test { public static void main(String[] args) { long startedTime = System.currentTimeMillis(); String message = null; String strValue1 = "value1"; String strValue2 = "value2"; String strValue3 = "value3"; String strValue4 = "value4"; String strValue5 = "value5"; for(int i = 0; i < 100000000; i++) { StringBuilder buffer = new StringBuilder(); buffer.append(strValue1); buffer.append(strValue2); buffer.append(strValue3); buffer.append(strValue4); buffer.append(strValue5); message = buffer.toString(); } long endedTime = System.currentTimeMillis(); System.out.println("所要時間(ms):" + (endedTime - startedTime)); }
}
・実行結果
所要時間(ms):9843
StringBuilderの初期化は新規インスタンスを作るのではなく、setLengthで0を設定した方が効率的です。
・ソースコード(setLengthに修正)
public class Test { public static void main(String[] args) { long startedTime = System.currentTimeMillis(); String message = null; String strValue1 = "value1"; String strValue2 = "value2"; String strValue3 = "value3"; String strValue4 = "value4"; String strValue5 = "value5"; StringBuilder buffer = new StringBuilder(); for(int i = 0; i < 100000000; i++) { buffer.setLength(0); buffer.append(strValue1); buffer.append(strValue2); buffer.append(strValue3); buffer.append(strValue4); buffer.append(strValue5); message = buffer.toString(); } long endedTime = System.currentTimeMillis(); System.out.println("所要時間(ms):" + (endedTime - startedTime)); } }
・実行結果
所要時間(ms):6374
約6.4秒。+演算子が4.6秒だったことを考えると、setLengthにしてもまだ遅いです。
さて、上のページでも解説されていますが、普通に考えれば+演算子結合の方が都度Stringオブジェクトが生成されて非効率的なはずです。では、なぜ+演算子の方が速いのでしょうか?
こういう時は我らが javap を使います。クラスファイルの中を覗いてみましょう。
まずは②StringBuilderさん。javap -c Test.class を実行してみます。
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
3: lstore_1
4: aconst_null
5: astore_3
6: ldc #3 // String value1
8: astore 4
10: ldc #4 // String value2
12: astore 5
14: ldc #5 // String value3
16: astore 6
18: ldc #6 // String value4
20: astore 7
22: ldc #7 // String value5
24: astore 8
26: new #8 // class java/lang/StringBuilder
29: dup
30: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
33: astore 9
35: iconst_0
36: istore 10
38: iload 10
40: ldc #10 // int 100000000
42: if_icmpge 103
45: aload 9
47: iconst_0
48: invokevirtual #11 // Method java/lang/StringBuilder.setLength:(I)V
51: aload 9
53: aload 4
55: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
58: pop
59: aload 9
61: aload 5
63: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
66: pop
67: aload 9
69: aload 6
71: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
74: pop
75: aload 9
77: aload 7
79: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
82: pop
83: aload 9
85: aload 8
87: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
90: pop
91: aload 9
93: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
96: astore_3
97: iinc 10, 1
100: goto 38
103: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
106: lstore 10
108: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
111: new #8 // class java/lang/StringBuilder
114: dup
115: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
118: ldc #15 // String 所要時間(ms):
120: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
123: lload 10
125: lload_1
126: lsub
127: invokevirtual #16 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
130: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
133: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
136: return
}
と、長々みてもわかりにくいので、
message = strValue1 + strValue2;
の部分だけ抜き出します。
Builderの場合は
buffer.append(strValue2);
ですね。
それがこの辺。
51: aload 9
53: aload 4
55: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
58: pop
59: aload 9
61: aload 5
55:を見るとStringBuilderのappendが呼び出されるのがわかります。
で、次は①+演算子の該当箇所。
36: new #9 // class java/lang/StringBuilder
39: dup
40: invokespecial #10 // Method java/lang/StringBuilder."<init>":()V
43: aload 4
45: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
48: aload 5
50: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
53: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
+演算子を使ったはずなのに、StringBuilderのインスタンスを作ってappendを2回使って最後にtoStringしています。
実はこのように、最新のJavaコンパイラは+演算子をStringBuilderに最適化してくれるんです。つまり、①+演算子をコンパイルした場合、実際に実行されるソースはこんな感じのものになります。
publicclassTest{
public static void main(String[] args) { long startedTime = System.currentTimeMillis(); String message = null; String strValue1 = "value1"; String strValue2 = "value2"; String strValue3 = "value3"; String strValue4 = "value4"; String strValue5 = "value5"; for(int i = 0; i < 100000000; i++) { message = new StringBuilder().append(strValue1).append(strValue2).toString(); message = new StringBuilder().append(message).append(strValue3).toString(); message = new StringBuilder().append(message).append(strValue4).toString(); message = new StringBuilder().append(message).append(strValue5).toString(); } long endedTime = System.currentTimeMillis(); System.out.println("所要時間(ms):" + (endedTime - startedTime)); } }
所要時間(ms):4562
実行結果が約4.6秒と、①+演算子の結果と変わらないことが確認できました。
つまり+演算子を使っているつもりが、内部的にはStringBuilderになっていたんです。
ついでにこの結果から、少なくともこのバージョンでは②StringBuilderのように単一のStringBuilderで1個ずつappendするより、使い捨てのStringBuilderでappendしてtoStringする方が速いことがわかりました。だからってわざわざこうしようとは思いませんが。
基本的にStringBuilderを使った方が良いことに間違いはないです(メンテナンス性とか考えると)。でも、+演算子の文字列結合を悪者扱いする必要もないということです。
こういう「ちょっと昔は駄目だったもの」が意外と改善されていたりするので、古い知識でモノを語るのは注意が必要ですね。
ちなみに上のサンプル、こうするとさらに速くなります。
public class Test { public static void main(String[] args) { long startedTime = System.currentTimeMillis(); String message = null; final String strValue1 = "value1"; final String strValue2 = "value2"; final String strValue3 = "value3"; final String strValue4 = "value4"; final String strValue5 = "value5"; for(int i = 0; i < 100000000; i++) { message = new StringBuilder().append(strValue1).append(strValue2).toString(); message = new StringBuilder().append(message).append(strValue3).toString(); message = new StringBuilder().append(message).append(strValue4).toString(); message = new StringBuilder().append(message).append(strValue5).toString(); } long endedTime = System.currentTimeMillis(); System.out.println("所要時間(ms):" + (endedTime - startedTime)); } }
所要時間(ms):3048
そして言うまでもないけどこれが最強。
public class Test { public static void main(String[] args) { long startedTime = System.currentTimeMillis(); String message = null; final String strValue1 = "value1"; final String strValue2 = "value2"; final String strValue3 = "value3"; final String strValue4 = "value4"; final String strValue5 = "value5"; for(int i = 0; i < 100000000; i++) { message = strValue1 + strValue2 + strValue3 + strValue4 + strValue5; } long endedTime = System.currentTimeMillis(); System.out.println("所要時間(ms):" + (endedTime - startedTime)); } }
所要時間(ms):1
関連エントリ:
Javaプログラマ必須 - 対話型にJavaを実行できる「Java REPL」 - Calms blog