StringBuilder で連結
このレッスンで分かること
StringBuilderは 可変 な文字列バッファ。+連結のループより圧倒的に速いappend(s)で末尾追加、toString()でStringに変換- スレッドセーフは
StringBuffer(synchronized版)、シングルスレッドはStringBuilder- Java 9+ なら
+もコンパイラが内部でinvokedynamic最適化するが、明示的に書くのが安全
StringBuilder で連結 とは
Java の
Stringは immutable で+連結はループ内で性能が落ちるため、可変な文字列ビルダーStringBuilderを使って効率良く文字列を組み立てる方法を学ぶ。
String は不変、だから連結が遅い
Java で文字列を表す String クラスには、他の言語をしばらく使ってきた人がぎょっとする特徴があります。それは「一度作った String の中身は 絶対に書き換えられない」ということです。これを イミュータブル (immutable) と呼びます。s.toUpperCase() を呼んでも、s 自身が大文字になるわけではなく、新しい大文字版の String が別オブジェクトとして返ってくるだけです。
String s = "abc"; s = s + "d";と書くと、変数sが指す先が"abc"から"abcd"に「付け替わった」だけで、元の"abc"の中身が伸びたわけではありません。つまり毎回新しいStringを作り直しています。
この性質は、セキュリティ (パスワードを安全に渡す) や スレッドセーフ (複数スレッドで共有しても壊れない) という意味では大きなメリットなのですが、for ループで文字を足していくような処理ではそのまま使うと 二次関数的に遅くなります。例えば 1 万回ループして 1 文字ずつ + で足していくと、累計のコピー量はおよそ 1 万 × 1 万 / 2 ≒ 5000 万文字に達してしまいます。
ここで登場するのが、可変な文字列バッファである java.lang.StringBuilder です。StringBuilder は内部に伸びる char[] を持っていて、append を呼ぶたびにその配列の末尾に書き込むだけなので、String を作り直す無駄が発生しません。
StringBuilder のキホン
StringBuilder は java.lang パッケージに入っているので、ArrayList と違って import 文は不要です。使い方は次のとおりです。
Java
public class Demo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("Java");
System.out.println(sb.toString()); // Hello, Java
}
}ポイントは次の流れです。まず new StringBuilder() で空のバッファを作る。次に append で末尾にどんどん追加する。最後に toString() で完成した文字列を取り出す、というシンプルな三段構えです。append は数値や boolean でもオーバーロードされているので、sb.append(123) や sb.append(true) のように何でも放り込めます。
StringBuilderはappendの戻り値として自分自身 (this) を返すので、sb.append("a").append("b").append("c")のように メソッドチェーン で書けます。Java のStreamや Kotlin の DSL でもよく見る、流れるような書き方ができるのは便利です。
初期容量を指定したいときは new StringBuilder(64) のように整数を渡せます。デフォルトは 16 文字分で、足りなくなると自動で 2 倍ずつ拡張されます。最終的にどれくらいの長さになるか分かっているときは、最初から指定しておくと拡張コピーの回数が減ります。
append 以外のメソッド
StringBuilder には末尾追加以外にも便利なメソッドがそろっています。最初に押さえておきたいのは次の 4 つです。
append(x)— 末尾に追加する。最頻出。数値やbooleanも OKinsert(index, x)— 指定位置に挿入する。sb.insert(0, "先頭に")で先頭追加delete(start, end)— 区間を削除する。endは含まない (String#substringと同じ流儀)reverse()— 中身を逆順にする。"abc"→"cba"ができる
Java
public class Demo2 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
sb.insert(5, ", Java"); // "Hello, Java"
sb.delete(0, 7); // "Java"
sb.reverse(); // "avaJ"
System.out.println(sb); // avaJ
}
}最後の System.out.println(sb) のように、println に渡したときは自動で toString() が呼ばれます。明示的に書きたいなら sb.toString() を呼んでください。String s = sb; のような直接代入は型が違うのでコンパイルエラーになります。
StringBuffer との違い
似た名前のクラスに StringBuffer があり、API もほとんど同じです。違いは スレッドセーフかどうか の一点で、次の表のとおりです。
| クラス | スレッドセーフ | 同期コスト | 推奨される使い方 |
|---|---|---|---|
StringBuilder | しない (非同期) | なし、高速 | 1 つのメソッド内で一時的に組み立てる用途 |
StringBuffer | する (synchronized) | あり、やや遅い | 複数スレッドから同じバッファを共有する用途 |
実際の開発では、ローカル変数として 1 メソッド内で使い切るケースが圧倒的に多いので、StringBuilder を選んでおけばまず間違いありません。StringBuffer は Java 1.0 から存在する古参で、StringBuilder は Java 5 で「ロック不要なら速い方を選びたい」というニーズに応えて追加されました。
現代の Java では、スレッド間で共有しない限りStringBuilderを使うのが定石です。マルチスレッドで共有しなければならないなら、そもそも設計を見直して、各スレッドに別のStringBuilderを持たせて最後に結合する、という方針のほうが速くて安全な場合が多いです。
よくある間違い
StringBuilder を使い始めたばかりの人がつまずきやすいポイントを 3 つ紹介します。
toString()の呼び忘れ —String result = sb;と書くとコンパイルエラーです。String result = sb.toString();のように明示的に呼び出してください。printlnのようにObject#toStringを内部で使ってくれるメソッドなら不要ですが、String型の変数に入れるときや、API の引数として渡すときは必ず変換が必要です- ループ外で
+連結を 1 回だけする場合にStringBuilderを持ち出す —String full = first + " " + last;のように、定数回 (2〜3 回) の連結なら、コンパイラが自動でStringBuilder相当のコードに変換してくれます。乱用するとかえって読みにくくなるので、forwhileなどのループ内で繰り返し連結するときに使うのが本来の使いどころです - 初期容量を指定せず巨大な文字列を組み立てる —
new StringBuilder()のデフォルト容量は 16 文字です。1 万文字以上を作ると分かっているなら、new StringBuilder(10_000)のように指定しておくと内部配列の再確保が発生せず高速です。性能を気にする場面では覚えておきましょう
もう 1 つ、append の引数として null を渡すと文字列の "null" がそのまま追加されてしまう点にも注意してください。例外にはなりませんが、意図しない出力になりがちです。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- CSV 出力 ─ 数百〜数万行を
+で繋ぐと激遅。StringBuilderでゼロから組み立てる - ログメッセージの動的構築 ─ デバッグログの長文を append でつなぐ
- JSON / XML 手組み ─ シリアライザを使わず軽く整形したいときの定番
- SQL の動的組み立て ─ プレースホルダ + 動的 WHERE 句を append で組む (SQL インジェクションには注意)
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
- ループ内で
+=連結 ─ O(n²) になりがち。StringBuilderへの置き換えがレビューの定番 - 初期容量を指定していない ─ 大量データなら
new StringBuilder(10000)で事前確保 - スレッド共有しているのに
StringBuilder─ マルチスレッドではStringBufferを使う、または synchronization toString()を呼び忘れる ─StringBuilderのまま渡すとStringを期待する API がObject.toString()で結果整合
パフォーマンス考慮事項
+=ループは O(n²)、StringBuilderは O(n) ─ 数千要素で 10〜100 倍差- 内部配列のリアロケート ─ 容量超過で 2 倍に拡張 + コピー。初期容量で防げる
StringBuilderのtoString()は新しいStringを生成 ─ 大きいバッファだとそこそこコストStringBufferのsynchronizedオーバーヘッド ─ 単一スレッドでは無駄。StringBuilderを選ぶ
ここまでの要点
ループ内連結は StringBuilder.append。初期容量を指定、最後に toString()。スレッド共有時のみ StringBuffer。
やってみよう
今回の課題は、文字列を指定回数だけ繰り返してつなげる repeat メソッドの実装です。次の手順で書いてみてください。
Solution.repeat(String s, int n)の中でnew StringBuilder()を作るforループでn回まわし、毎回sb.append(s)を呼ぶ- ループが終わったら
return sb.toString();で結果を返す
repeat("ab", 3) なら "ababab"、repeat("x", 5) なら "xxxxx" になります。n が 0 のとき、または s が空文字 "" のときは、ループが回らない / 何も追加されないので、最終結果は "" (空文字) になります。特別な分岐を書かなくても、StringBuilder を素直に使えば自然と空文字が返ります。
慣れてきたら、+ 連結版 (String result = ""; for (...) result += s;) と速度を比べてみてください。n = 100_000 くらいにすると、StringBuilder 版が数ミリ秒で終わるのに対し、+ 連結版は数秒〜数十秒かかることもあります。実際に手を動かして体感すると、なぜ StringBuilder が必要なのかが腑に落ちるはずです。
次のレッスンでは、String#split や String.format といった、組み立て以外の文字列操作にも触れていきます。まずはここで「ループ内で連結するなら StringBuilder」というセオリーをしっかり身につけておきましょう。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は 正規表現の基本 で、正規表現の基本 を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- StringBuilder の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. StringBuilder とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
StringBuilderをnew StringBuilder()で生成して使うこと (+連結だけで実装しない)forなどのループでsをn回appendすること- 最後に
toString()でStringに変換してからreturnすること
入出力例
test-cases.txt
repeat("ab", 3) → "ababab"
repeat("x", 5) → "xxxxx"
repeat("hi", 0) → ""
repeat("", 5) → ""
repeat("Java", 2) → "JavaJava"
repeat("hello", 1) → "hello"