StringBuilder で連結
1 万件つないだところで、急に遅くなる
ログの行やレポートの本文を組み立てる処理は、書いているうちは何ということもありません。ところがデータが 100 件から 10 万件に増えたとたん、同じコードが数秒から数十秒かかるようになります。
原因はほぼ 1 つで、ループの中で + を使って文字列をつないでいることです。
Java
String result = "";
for (String name : names) {
result = result + name + ",";
}見た目は素直ですが、これは要素数が増えると二次関数的に遅くなります。
+ は毎回、まるごと新しい文字列を作り直している
Java の String は、一度作ったら中身を書き換えられません。s.toUpperCase() を呼んでも s 自身が大文字になるのではなく、大文字版の別オブジェクトが返ってくるだけです。この性質をイミュータブルと言います。
つまり result = result + name は、result の末尾に文字を書き足しているのではありません。それまでの result を全部コピーし、name をつなげた新しい String を作り、その新しい方を result に指し直しています。
1 万回まわせば、1 万個の使い捨て文字列ができます。しかも 1 回あたりのコピー量は回を追うごとに増えるので、合計は 5000 万文字規模になります。遅いのは当たり前で、その分だけ回収の負担も増えます。
中身を書き換えられる箱を、1 つだけ用意する
StringBuilder は、伸びる配列を内側に持った書き換え可能な文字列です。append を呼ぶと、新しいオブジェクトを作らずにその配列の末尾へ書き足します。java.lang にあるので import は要りません。
Java
public class Demo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("id=").append(42);
sb.append(" name=").append("java");
String line = sb.toString();
System.out.println(line); // id=42 name=java
}
}append は自分自身を返すので、上のように点でつないで書けます。ループの中で何度 append を呼んでも、箱は最初に作った 1 つのままです。増えるのは中の配列の中身だけなので、件数に比例した時間で終わります。
最後の toString() を忘れないでください。
Java
String ng = sb; // コンパイルエラー
String ok = sb.toString(); // これで String になるsb は文字列を組み立てるための道具で、String そのものではありません。System.out.println(sb) のように内部で toString が呼ばれる場面だけは例外です。
よく似た
StringBufferは、複数のスレッドから同時に触られても壊れない代わりに少し遅い版です。1 つのメソッドの中で組み立てて捨てる普通の使い方ならStringBuilderを選びます。
なお、first + " " + last のような 2 回か 3 回きりの連結は、コンパイラが自動で同じ仕組みに置き換えてくれます。StringBuilder を持ち出すべきなのは、あくまでループの中で繰り返しつなぐときです。
要件
StringBuilderをnew StringBuilder()で生成して使うこと (+連結だけで実装しない)forなどのループでsをn回appendすること- 最後に
toString()でStringに変換してからreturnすること
入出力例
repeat("ab", 3) → "ababab"
repeat("x", 5) → "xxxxx"
repeat("hi", 0) → ""
repeat("", 5) → ""
repeat("Java", 2) → "JavaJava"
repeat("hello", 1) → "hello"