Java のプログラム内で文字列を生成する際に、どのようなやり方が最も速度が速いか、を測定してみました。
#想定
Java プログラム内で 以下のような SQL 文字列を作る想定
SELECT
NAME
,SECTION
,ADD_DATE
,DEL_DATE
FROM
MEMBER_MST
#測定に使用した Java バージョン
Java 7(1.7.0_45)
Java 6(1.6.0_45)
Java 5(1.5.0_22)
Java 1.4(1.4.2_19)
#測定マシンスペック
OS: Windows 7 Pro. SP1 (32 bit)
CPU:Intel Core i3 540
RAM:4 GB
#測定方法
以下の各ソースコードを 10万回ループして 開始時と終了時の System.nanoTime() の値の差分を集計しました。
ただし Java 1.4 には System.nanoTime() が存在しないので、System.currentTimeMillis() で集計。
(そのため、Java 1.4 における測定値は参考程度と考えてください)
#測定結果
「オリジナル」が実行するソースコードです。
「コンパイル後」が class ファイルから jad で復元したソースコードです。
String (リテラル文字列をそのまま連結)
java.lang.String のインスタンスを用意して、リテラル文字列同士を + で結合するパターン。
###オリジナル:
String str =
" SELECT"
+ " NAME"
+ " ,SECTION"
+ " ,ADD_DATE"
+ " ,DEL_DATE"
+ " FROM"
+ " MEMBER_MST ";
###測定結果:
7 :99 μsec
6 :102 μsec
5 :200 μsec
1.4:222 μsec
###コンパイル後:
String str = " SELECT NAME ,SECTION ,ADD_DATE ,DEL_DATE FROM MEMBER_MST ";
※ その他のバージョンも全て同じコード
###結論:
String に 連結されたリテラル文字列が代入されるだけなので、ダントツで速い
String (リテラル文字列を連結するが、一部 String を代入する)
java.lang.String のインスタンスを用意して、リテラル文字列同士を + で結合するパターンだが、一部に String 型の変数が入る。
結果の文字列を等しくするため、"ADD_DATE" を変数としていますが、WHERE句 をプログラムで切り替える、といったパターンを想定しています。
###オリジナル:
String fixed = "ADD_DATE";
String str =
" SELECT"
+ " NAME"
+ " ,SECTION"
+ " ," + fixed
+ " ,DEL_DATE"
+ " FROM"
+ " MEMBER_MST ";
###測定結果:
7 :21,750 μsec
6 :29,486 μsec
5 :40,218 μsec
1.4:50,889 μsec
###コンパイル後:
String fixed = "ADD_DATE";
String str = (new StringBuilder(" SELECT NAME ,SECTION ,")).append(fixed).append(" ,DEL_DATE").append(" FROM").append(" MEMBER_MST ").toString();
※ 5 から 7 まで同じコード
String fixed = "ADD_DATE";
String str = " SELECT NAME ,SECTION ," + fixed + " ,DEL_DATE" + " FROM" + " MEMBER_MST ";
###結論:
fixed が出てくるまでは全て連結されるが、その後は StringBuilder の append で連結される。
StringBuilder を new するコストと、append メソッドコールのオーバーヘッド分、時間がかかるが、全体としては2番目の早さ。
StringBuilder の append を都度呼ぶよりは速い(ただし書き方にもよる。先頭に fixed がある場合はあまり変わらないと思われる)。
1.4 は StringBuilder サポート以前のバージョンなので、StringBuilder は未使用だが、5以降と同じロジックで連結される。
String ( += で連結する)
java.lang.String のインスタンスを用意して、リテラル文字列を += String のインスタンスに代入する。
###オリジナル:
String str = "";
str += " SELECT";
str += " NAME";
str += " ,SECTION";
str += " ,ADD_DATE";
str += " ,DEL_DATE";
str += " FROM";
str += " MEMBER_MST ";
###測定結果:
7 :77,915 μsec
6 :96,035 μsec
5 :129,927 μsec
1.4:171,667 μsec
###コンパイル後:
String str = "";
str = (new StringBuilder(String.valueOf(str))).append(" SELECT").toString();
str = (new StringBuilder(String.valueOf(str))).append(" NAME").toString();
str = (new StringBuilder(String.valueOf(str))).append(" ,SECTION").toString();
str = (new StringBuilder(String.valueOf(str))).append(" ,ADD_DATE").toString();
str = (new StringBuilder(String.valueOf(str))).append(" ,DEL_DATE").toString();
str = (new StringBuilder(String.valueOf(str))).append(" FROM").toString();
str = (new StringBuilder(String.valueOf(str))).append(" MEMBER_MST ").toString();
※ 5 から 7 まで同じコード
String str = "";
str = str + " SELECT";
str = str + " NAME";
str = str + " ,SECTION";
str = str + " ,ADD_DATE";
str = str + " ,DEL_DATE";
str = str + " FROM";
str = str + " MEMBER_MST ";
###結論:
+= は StringBuilder インスタンスを、すでにある String のインスタンスから逐一生成して append するため、かなり遅い。
ループ回数が多い箇所では使用しないほうが懸命。
StringBuffer
StringBuffer のインスタンスを一つ生成し、append メソッドで結合していくパターン
###オリジナル:
StringBuffer sb = new StringBuffer();
sb.append(" SELECT");
sb.append( " NAME");
sb.append(" ,SECTION");
sb.append(" ,ADD_DATE");
sb.append(" ,DEL_DATE");
sb.append(" FROM");
sb.append( " MEMBER_MST ");
###測定結果:
7 :45,493 μsec
6 :53,401 μsec
5 :66,073 μsec
1.4:71,667 μsec
###コンパイル後:
StringBuffer sb = new StringBuffer();
sb.append(" SELECT");
sb.append(" NAME");
sb.append(" ,SECTION");
sb.append(" ,ADD_DATE");
sb.append(" ,DEL_DATE");
sb.append(" FROM");
sb.append(" MEMBER_MST ");
※ その他のバージョンも全て同じコード
###結論:
全てのバージョンでコンパイル後もコードは同じ。 StringBuilder よりは遅いが String を += で連結するよりは速い。
StringBuilder とインターフェースはほぼ同じなのにこちらの方が遅い理由はスレッドセーフにするため、内部で synchronized しているからとのこと。
StringBuilder
StringBuilder のインスタンスを一つ生成し、append メソッドで結合していくパターン
###オリジナル:
StringBuilder sb = new StringBuilder();
sb.append(" SELECT");
sb.append( " NAME");
sb.append(" ,SECTION");
sb.append(" ,ADD_DATE");
sb.append(" ,DEL_DATE");
sb.append(" FROM");
sb.append( " MEMBER_MST ");
###測定結果:
7 :26,625 μsec
6 :36,955 μsec
5 :48,191 μsec
1.4:StringBuilder が存在しないため測定不可
###コンパイル後:
StringBuilder sb = new StringBuilder();
sb.append(" SELECT");
sb.append(" NAME");
sb.append(" ,SECTION");
sb.append(" ,ADD_DATE");
sb.append(" ,DEL_DATE");
sb.append(" FROM");
sb.append(" MEMBER_MST ");
※ その他のバージョンも全て同じコード
###結論:
StringBuffer よりも高速だが、append メソッドコールにオーバーヘッドがかかるため、固定文字列を連結する場合は、リテラル文字列をそのまま連結した方が速い(Java7 だと268倍違う)
追記:
StringBuilder (初期容量あり)
StringBuilder のインスタンスを一つ生成し、append メソッドで結合していくパターンだが、
StringBuilder のコンストラクタで初期容量を設定する。
今回は作成する文字列長ピッタリの 60 を初期容量に与える。
###オリジナル:
StringBuilder sb = new StringBuilder(60);
sb.append(" SELECT");
sb.append( " NAME");
sb.append(" ,SECTION");
sb.append(" ,ADD_DATE");
sb.append(" ,DEL_DATE");
sb.append(" FROM");
sb.append( " MEMBER_MST ");
###測定結果:
7 :20,691 μsec
6 :30,794 μsec
5 :38,024 μsec
1.4:StringBuilder が存在しないため測定不可
###コンパイル後:
StringBuilder sb = new StringBuilder(60);
sb.append(" SELECT");
sb.append(" NAME");
sb.append(" ,SECTION");
sb.append(" ,ADD_DATE");
sb.append(" ,DEL_DATE");
sb.append(" FROM");
sb.append(" MEMBER_MST ");
※ その他のバージョンも全て同じコード
###結論:
StringBuilder は適切な初期容量をあたえた方が高速。作成後のおおよその文字列長がわかっている場合はこちらの方が良さそう。
ただし、上記のケースで初期容量として 1000 を与えた場合は、Java7 で おおよそ 66000 μsec だった(引数を与えない方が速い)ので
大きすぎる値の場合は逆効果となる。(おそらく初期バッファ確保に時間がかかっていると思われる)