StringBuilder で連結

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 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 万文字に達してしまいます。

diagram (will load when visible)

ここで登場するのが、可変な文字列バッファである java.lang.StringBuilder です。StringBuilder は内部に伸びる char[] を持っていて、append を呼ぶたびにその配列の末尾に書き込むだけなので、String を作り直す無駄が発生しません。

StringBuilder のキホン

StringBuilderjava.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) のように何でも放り込めます。

StringBuilderappend の戻り値として自分自身 (this) を返すので、sb.append("a").append("b").append("c") のように メソッドチェーン で書けます。Java の Stream や Kotlin の DSL でもよく見る、流れるような書き方ができるのは便利です。

初期容量を指定したいときは new StringBuilder(64) のように整数を渡せます。デフォルトは 16 文字分で、足りなくなると自動で 2 倍ずつ拡張されます。最終的にどれくらいの長さになるか分かっているときは、最初から指定しておくと拡張コピーの回数が減ります。

append 以外のメソッド

StringBuilder には末尾追加以外にも便利なメソッドがそろっています。最初に押さえておきたいのは次の 4 つです。

  • append(x) — 末尾に追加する。最頻出。数値や boolean も OK
  • insert(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 相当のコードに変換してくれます。乱用するとかえって読みにくくなるので、for while などのループ内で繰り返し連結するときに使うのが本来の使いどころです
  • 初期容量を指定せず巨大な文字列を組み立てるnew StringBuilder() のデフォルト容量は 16 文字です。1 万文字以上を作ると分かっているなら、new StringBuilder(10_000) のように指定しておくと内部配列の再確保が発生せず高速です。性能を気にする場面では覚えておきましょう

もう 1 つ、append の引数として null を渡すと文字列の "null" がそのまま追加されてしまう点にも注意してください。例外にはなりませんが、意図しない出力になりがちです。

diagram (will load when visible)

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • 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 倍に拡張 + コピー。初期容量で防げる
  • StringBuildertoString() は新しい String を生成 ─ 大きいバッファだとそこそこコスト
  • StringBuffersynchronized オーバーヘッド ─ 単一スレッドでは無駄。StringBuilder を選ぶ
この章のポイント

ここまでの要点 ループ内連結は StringBuilder.append。初期容量を指定、最後に toString()。スレッド共有時のみ StringBuffer

やってみよう

今回の課題は、文字列を指定回数だけ繰り返してつなげる repeat メソッドの実装です。次の手順で書いてみてください。

  1. Solution.repeat(String s, int n) の中で new StringBuilder() を作る
  2. for ループで n 回まわし、毎回 sb.append(s) を呼ぶ
  3. ループが終わったら return sb.toString(); で結果を返す

repeat("ab", 3) なら "ababab"repeat("x", 5) なら "xxxxx" になります。n0 のとき、または s が空文字 "" のときは、ループが回らない / 何も追加されないので、最終結果は "" (空文字) になります。特別な分岐を書かなくても、StringBuilder を素直に使えば自然と空文字が返ります。

慣れてきたら、+ 連結版 (String result = ""; for (...) result += s;) と速度を比べてみてください。n = 100_000 くらいにすると、StringBuilder 版が数ミリ秒で終わるのに対し、+ 連結版は数秒〜数十秒かかることもあります。実際に手を動かして体感すると、なぜ StringBuilder が必要なのかが腑に落ちるはずです。

次のレッスンでは、String#splitString.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 つができることを確認しましょう。

  1. StringBuilder の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. StringBuilder とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. StringBuildernew StringBuilder() で生成して使うこと (+ 連結だけで実装しない)
  2. for などのループで snappend すること
  3. 最後に 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"

ヒント

main.java
main.java
学習モード

メモ

StringBuilder で連結

⌘S で保存