はじめに
随分昔(※Java1.5の頃)から、「文字列結合は"+"ではなく、StringBufferを使うと処理が高速化する」ということが既に広く知られていました。
そのため、今でも文字列結合は基本的にStringBufferやStringBuilderを使っていますが、「Stringクラスのconcatメソッドの処理速度はどの程度だろう?」とふと疑問に思ったので、今回はこれらの処理速度を比較してみました。
テスト環境
- PC
- CPU:Core-i5 8250U
- RAM:8GB
- JDK:Java-1.8.0_191
テストに使用したコード
- "abc"という文字を10万回結合するコードを作成しました。
- 当初は100万回結合するコードにしていましたが、処理が中々終わらなかったので結合回数を減らしました。
- コンパイルはJava1.8準拠で行いました。
StringTest.java
public class StringTest {
public static void main(String[] args) {
plus();
concat();
builder();
buffer();
}
public static void plus() {
long start = System.currentTimeMillis();
String txt = "abc";
for (int i=0; i<100000; i++) {
txt += "abc";
}
long end = System.currentTimeMillis();
System.out.println("+:" + (end - start) + "ms");
}
public static void concat() {
long start = System.currentTimeMillis();
String txt = "abc";
for (int i=0; i<100000; i++) {
txt = txt.concat("abc");
}
long end = System.currentTimeMillis();
System.out.println("concat:" + (end - start) + "ms");
}
public static void builder() {
long start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder("abc");
for (int i=0; i<100000; i++) {
sb.append("abc");
}
String txt = sb.toString();
long end = System.currentTimeMillis();
System.out.println("StringBuilder:" + (end - start) + "ms");
}
public static void buffer() {
long start = System.currentTimeMillis();
StringBuffer sb = new StringBuffer("abc");
for (int i=0; i<100000; i++) {
sb.append("abc");
}
String txt = sb.toString();
long end = System.currentTimeMillis();
System.out.println("StringBuffer:" + (end - start) + "ms");
}
}
テスト結果
処理内容 | 処理時間 |
---|---|
+による結合 | 7439ms |
String.concat()による結合 | 2990ms |
StringBuilderによる結合 | 2ms |
StringBufferによる結合 | 4ms |
テスト結果の考察
当たり前レベルの話
- 「+による結合」では、「"+"で文字列を結合させる度に新しい文字列オブジェクトを作り、その参照を変数(txt)にセットする」ために処理が遅くなっています。
- Javaに限らず、オブジェクトの生成はコストがかかる...
- String型のオブジェクトはイミュータブル(immutable:変更できない)なので、このような防衛的コピー(defensive copying)が行われています。
- StringBuilderとStringBufferは文字列バッファ(可変の文字列)に文字列を追加しているだけで、新たなオブジェクトが生成されないので、「+による結合」と比べて圧倒的に高速です。
- StringBuilderはスレッドアンセーフ(※同期化は保証されない)、StringBufferはスレッドセーフ(※同期化が保証される)なので、StringBufferの方が(誤差レベルですが)わずかに処理が遅くなっています。
なぜ「+による結合」より、String.concat()が少し速いのか?
String.concat()
- String.concat()のソースを見ると、以下のように「2つのchar型の配列を結合して、最後にString型のオブジェクトに変換する」という処理になっています。
- つまり、オブジェクトの生成は「最後のString型オブジェクトの生成」の1回となります。
String.classの抜粋
// Eclipse Class Decompiler pluginを使って得られたソースです。
public final class String implements Serializable, Comparable<String>, CharSequence {
private final char[] value;
public String concat(String arg0) {
int arg1 = arg0.length();
if(arg1 == 0) {
return this;
} else {
int arg2 = this.value.length;
char[] arg3 = Arrays.copyOf(this.value, arg2 + arg1);
arg0.getChars(arg3, arg2);
return new String(arg3, true);
}
}
void getChars(char[] arg0, int arg1) {
System.arraycopy(this.value, 0, arg0, arg1, this.value.length);
}
}
+による結合
- 一方で「+による結合」は、公式ドキュメントに書かれているように「StringBuilder.append()で文字列を結合した後、String型のオブジェクトに変換する」という処理になっているようです。
- つまり、オブジェクトの生成は「StringBuilderオブジェクトの生成」と「最後のString型オブジェクトの生成」で計2回行われていることになります。
- こちらのサイトによれば、さらにString.valueOf()が内部的に呼び出されているそうです。
Java言語は、文字列連結演算子( + )、およびその他のオブジェクトから文字列への変換に対する特別なサポートを提供します。文字列連結はStringBuilder (またはStringBuffer)クラスとそのappendメソッドを使って実装されています。
まとめ
- 文字列結合は、定石通りにStringBuilderかStringBufferを積極的に使うべき。
- スレッドセーフにする必要があるなら、必ずStringBufferを使う。
- 文字列結合が1回だけでも、"+"ではなくString.concat()を使った方が良さそう。
- 今までは「+による結合」とString.concat()のパフォーマンスは大して変わらないと思っていましたが、意外な結果に驚きました。
- ちょっとしたパフォーマンスの差でも、"チリツモ"でシステム全体のパフォーマンスが多少変わるかもしれないので...