Java

JavaのStringBuilderとは:使い方とStringBufferとの違い、+連結との使い分けをコードで解説

JavaのStringBuilderとは:使い方とStringBufferとの違い、+連結との使い分けをコードで解説

StringBuilderは、Javaで文字列を効率よく組み立てるためのクラスです。変更できないStringと違い、同じインスタンスのまま文字を追加・挿入できるため、ループでの連結やログ・CSVの生成で使われます。この記事では、append・insert・改行・repeatといった基本操作、StringやStringBufferとの違い、そして「いつ明示的に使うべきか」までを、公式のJavadocと実際に動くコードで整理します。記載の仕様は2026年10月時点のJava 25(LTS)のAPIドキュメントで確認したものです。

まとめ:StringBuilderの要点と、明示して使うべき場面の見分け方

  • 役割:可変(mutable)な文字列バッファ。Stringのように変更のたびに新しいオブジェクトを作らない。
  • 基本操作:末尾追加はappend、途中挿入はinsert、改行はSystem.lineSeparator()か"\n"、同じ文字の繰り返しはJava 21以降のrepeat。appendは戻り値が自身なのでメソッドチェーンできる。
  • StringBufferとの違い:StringBufferは同期(スレッドセーフ)で、StringBuilderは非同期。単一スレッドならStringBuilderが速い。
  • 使うべき場面:+を1つの式で書くだけの単純連結は、Java 9以降のコンパイラが効率的な連結処理に変換するため不要。ループ内や条件分岐で文字列を積み上げるときに明示する価値がある。
  • 落とし穴:equalsは中身でなく参照を比べる。中身の比較はcompareTo(Java 11以降)かtoString()同士で行う。

以下、仕組み・操作・判断基準の順に見ていきます。

StringBuilderの仕組みと、不変のStringで連結を重ねると遅くなる理由

StringBuilderとは、Javaで可変な文字列を組み立てるためのクラスです。なぜこれが必要になるのかは、Stringの性質から説明できます。JavaのStringは不変(immutable)です。str += "x"のように見える変更も、内部では毎回新しいStringインスタンスが生成されています。短い処理なら問題になりませんが、何千回も連結する場合は、生成と破棄の積み重なりによるメモリ消費と処理時間の増加が問題です。捨てられた中間オブジェクトはヒープに溜まり、Javaのガベージコレクション(GC)の仕組みと種類で解説している回収処理の負担として跳ね返ってきます。

StringBuilderは内部に文字の配列(バッファ)を1つ持ち、それを書き換えながら文字列を伸ばします。新しいオブジェクトを作らないため、繰り返しの組み立てに向きます。最後にtoString()を呼んだ時点で確定したStringを取り出す流れです。StringBuilderのJavadoc(Java 25)には「Since: 1.5」とあり、J2SE 5.0で追加されたクラスだとわかります。同じページには、単一スレッドで使うバッファではStringBufferより優先して使うよう推奨する記述もあります。

StringBuilder sb = new StringBuilder();
sb.append("Hello, ").append("World!");
String result = sb.toString();  // "Hello, World!"

内部の文字配列は、Java 9で入ったJEP 254(Compact Strings)以降、中身がLatin-1の範囲に収まる間は1文字1バイトで保持され、日本語などが入るとUTF-16へ切り替わります。利用者がこの違いを意識する必要はありませんが、英数字中心の組み立てではメモリ消費が小さく済みます。

String・StringBuilder・StringBufferの可変性と同期の2軸比較

この3つは「文字列を扱う」点で混同されがちですが、可変性とスレッド安全性で役割が分かれます。検索でも「stringbuilder stringbuffer 違い」「string stringbuilder 違い」がよく調べられる論点です。

クラス 可変性 スレッドセーフ 速度 主な用途
String 不変 不変ゆえ安全 連結の繰り返しは低速 変更しない文字列
StringBuilder 可変 なし 速い 単一スレッドでの組み立て
StringBuffer 可変 あり(同期) 同期の分だけ遅い 複数スレッドで共有するバッファ

StringBuilderとStringBufferはAPIがほぼ同じで、どちらも内部実装(package-privateのAbstractStringBuilder)を共有しています。違いは、StringBufferのJavadocにあるとおり、各メソッドがsynchronizedで同期される点です。同期にはコストがあるため、1つのスレッドからしか触らないバッファならStringBuilderを選びます。複数スレッドが同じインスタンスへ同時に書き込む構成のときだけStringBufferを検討してください。なお、スレッドごとにバッファを分けて持てるなら、その場合もStringBuilderで十分です。

仮想スレッドを使う構成では、Java 21〜23でsynchronizedの中でブロックすると仮想スレッドがキャリアスレッドに固定(pinning)される制約がありました。これはJEP 491によりJava 24で解消されています。StringBufferの同期は短時間で終わるため実害は小さいものの、仮想スレッドとの組み合わせはJava Virtual Thread(仮想スレッド)の作り方と落とし穴もあわせて確認しておくと安心です。

StringBuilderの使い方をコードで確認する:生成から取り出しまでの基本操作

初期容量16文字の既定値と、容量を指定して再確保を減らす書き方

引数なしのコンストラクタは、初期容量16文字のバッファを確保します。保持する文字列が確保済みの容量を超えたときは、内部配列の再確保が必要です。JavadocのensureCapacityの説明では、新しい容量は「要求された最小容量」と「旧容量の2倍+2」の大きいほうと定められています。最終的な長さの見当がつくなら、最初から容量を指定すると再確保とコピーの回数を減らせます。文字列を渡して初期化することもでき、その場合の初期容量は「文字列の長さ+16」です。

StringBuilder sb1 = new StringBuilder();         // 初期容量16文字
StringBuilder sb2 = new StringBuilder(1024);     // 大きめに確保し再確保を抑える
StringBuilder sb3 = new StringBuilder("接頭辞"); // 容量は 3 + 16 = 19
System.out.println(sb1.capacity()); // 16
System.out.println(sb3.capacity()); // 19

appendでの末尾追加とメソッドチェーン、nullが4文字のnullになる挙動

appendは引数を末尾に追加します。文字列だけでなくint・boolean・任意のオブジェクトも受け取り、オブジェクトの場合はString.valueOfと同じ変換結果が連結されます。戻り値は自分自身です。そのため、同じインスタンスへの追加操作を続けて.append(...)とつなげられます。

StringBuilder sb = new StringBuilder();
sb.append("数量:").append(100).append(true);  // "数量:100true"
String s = null;
sb.append(s);  // 例外ではなく "null" の4文字が追加される

注意したいのはnullの扱いです。append((String) null)はNullPointerExceptionにならず、文字列"null"を連結します。CSVやログに意図せず"null"が混ざる原因になりやすいので、変数がnullになり得る箇所ではObjects.toString(s, "")のように既定値を決めてから追加してください。

改行の追加は「\n」とSystem.lineSeparator()を出力先で使い分け

改行はappend("\n")でLF(ラインフィード)を入れられます。Windowsのテキストとして開く前提なら"\r\n"(CRLF)、実行環境の改行コードに合わせたいならSystem.lineSeparator()、と用途で使い分けます。ファイル形式で改行コードが決まっている場合(RFC 4180のCSVはCRLF、多くの設定ファイルはLF)は、実行環境に依存させず固定の文字を書くほうが安全です。

StringBuilder sb = new StringBuilder();
sb.append("1行目").append(System.lineSeparator()); // 実行環境の改行
sb.append("2行目").append('\n');                    // 常に LF

insertで指定位置へ挿入する方法と、範囲外で起きる例外の防ぎ方

insertは第1引数のインデックス位置に第2引数を割り込ませます。末尾に足すappendと違い、途中へ差し込めるのが特徴です。指定位置が0未満か現在の長さを超えるとStringIndexOutOfBoundsExceptionが発生するため、可変長のデータを扱うときは0 <= index && index <= sb.length()を確かめてから呼びます。

StringBuilder sb = new StringBuilder("Hello World");
sb.insert(5, " Java");  // "Hello Java World"
sb.insert(0, '[');      // 先頭への挿入も可
sb.insert(100, "x");    // 長さを超える位置 → StringIndexOutOfBoundsException

delete・replace・reverse・setLengthによる内容変更

そのほかよく使うメソッドを挙げます。範囲削除のdelete、1文字削除のdeleteCharAt、範囲置換のreplace、反転のreverse、現在の文字数を返すlength、長さを切り詰めるsetLength、確定したStringを取り出すtoStringです。lengthとtoString以外は同じインスタンスを破壊的に変更します。reverseはサロゲートペア(絵文字など)を1文字として扱うため、反転で文字が壊れることはありません。各メソッドの引数や戻り値の正確な仕様はJavadocで確認でき、Javadocそのものの読み方と書き方はJavadocの書き方(コメント・タグの記述例と生成方法)で解説しています。

StringBuilder sb = new StringBuilder("abcdef");
sb.delete(0, 2);        // "cdef"(0以上2未満を削除)
sb.replace(0, 1, "C");  // "Cdef"
sb.reverse();           // "fedC"
int len = sb.length();  // 4
sb.setLength(0);        // 中身を空にする(容量は保持)

Java 21で追加されたrepeatで、同じ文字列を指定回数だけ追加する

Java 21ではrepeat(int codePoint, int count)とrepeat(CharSequence cs, int count)が追加されました(Javadocの表記は「Since: 21」)。区切り線やインデント、固定長のパディングをループなしで書けます。countが負の値ならIllegalArgumentExceptionになります。Java 17以前を対象にするコードでは使えないため、ビルドの--release指定とあわせて判断してください。

StringBuilder sb = new StringBuilder();
sb.repeat('-', 10);          // "----------"
sb.append('\n').repeat("ab", 3); // 改行のあとに "ababab"

そのまま動かせるサンプル:CSV生成とバッファの再利用を1ファイルで試す

ここまでの操作を1つのファイルにまとめました。Java 11以降はJEP 330により、ソースファイルをコンパイルせずjava StringBuilderDemo.javaで直接実行できます。repeatを含むためJava 21以降で動かしてください。

import java.util.List;

public class StringBuilderDemo {
    public static void main(String[] args) {
        List<String[]> rows = List.of(
            new String[] {"id", "name", "qty"},
            new String[] {"1", "りんご", "3"},
            new String[] {"2", "みかん", "12"});

        // 1. CSVを組み立てる(RFC 4180に合わせて改行はCRLF)
        StringBuilder sb = new StringBuilder(64);
        for (String[] row : rows) {
            sb.append(String.join(",", row)).append("\r\n");
        }
        System.out.print(sb);

        // 2. setLength(0)で空にし、同じバッファを再利用する
        sb.setLength(0);
        sb.append("total=").append(15);
        sb.insert(0, '[').append(']');
        System.out.println(sb);                 // [total=15]

        // 3. Java 21以降のrepeat
        sb.setLength(0);
        System.out.println(sb.repeat('-', 10)); // ----------

        // 4. equalsは参照比較、中身の比較はcompareToかtoString
        StringBuilder a = new StringBuilder("java");
        StringBuilder b = new StringBuilder("java");
        System.out.println(a.equals(b));                       // false
        System.out.println(a.compareTo(b) == 0);               // true
        System.out.println(a.toString().equals(b.toString())); // true
    }
}

実行すると、3行のCSV、[total=15]、10個のハイフン、false・true・trueの順に出力されます。System.out.print(sb)のようにStringBuilderをそのまま渡せるのは、print(Object)が内部でString.valueOfを呼ぶためです。

+連結とStringBuilderの使い分け:Java 9以降のコンパイラの挙動から判断

「連結は必ずStringBuilderを使う」という説明をよく見かけますが、Java 9以降ではそのまま当てはまりません。Javaマイグレーションの進め方(JDK 8から21/25への手順)で扱う版の更新の中で、連結処理の仕組みが変わっているためです。

単純な+連結はJEP 280のinvokedynamicで処理され、手書きは不要

Java 9で導入されたJEP 280(Indify String Concatenation)により、a + b + cのような+連結を、javacはinvokedynamic経由でStringConcatFactoryを呼ぶコードへコンパイルします。Java 8までのように裏でStringBuilderの呼び出し列へ展開するのではなく、実行時のJDKが効率のよい連結の方法を選ぶ仕組みです。したがって、1つの式で完結する単純な連結を、わざわざStringBuilderへ手で書き換える必要はありません。むしろ手で書くと、JDKの更新で連結処理が改善されても恩恵を受けにくくなります。

ただし、この変換はjavacの出力先が9以上のときだけ働きます。--release 8でコンパイルしたクラスは、新しいJDKで動かしても旧来のStringBuilder展開のままです。Java 8向けの成果物を流用し続けている現場では、再コンパイルまでが移行作業に含まれます。

javapでバイトコードを見て、+連結がどう変換されたかを確かめる手順

自分の環境で変換結果を確かめるには、JDK付属のjavapで逆アセンブルします。次のクラスをコンパイルしてjavap -cを実行すると、Java 9以降の出力先ではinvokedynamicの行にmakeConcatWithConstantsが現れ、--release 8ではStringBuilder.appendの呼び出しが並びます。

// Concat.java
public class Concat {
    static String join(String a, int b) { return a + ":" + b; }
}

// 端末で実行
javac Concat.java && javap -c Concat
javac --release 8 Concat.java && javap -c Concat

ループ内で文字列を積み上げる処理こそStringBuilderを明示する場面

コンパイラが変換できるのは「1つの式」です。ループのように繰り返しのたびに連結が分かれる場合は、各回が独立した連結となり、反復ごとに中間のStringが生成されます。ここがStringBuilderを明示すべき本命です。

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i).append(',');
}
String csv = sb.toString();

この処理をString csv = ""; csv += ...;で書くと、毎回それまでの全文字をコピーした新しいStringが作られるため、処理量は件数のおおむね2乗で増えます。件数が増えるほど差が開くので、ループでの連結はStringBuilderを選んでください。条件分岐で文字列を継ぎ足していくケースも同様です。

区切り連結でのString.joinとCollectors.joiningの選択

区切り文字でつなぐだけなら、StringBuilderを書かずに済むことも多くあります。固定要素や配列の連結はString.join、コレクションやストリームの連結はCollectors.joiningが読みやすく、Javaラムダ式とStream APIの使いどころで扱う書き方とも相性の良い選択です。接頭辞と接尾辞を付けながら少しずつ足したいときはStringJoinerも使えます。

String csv = String.join(",", "A", "B", "C");  // "A,B,C"
String joined = list.stream().collect(Collectors.joining(", ", "[", "]")); // "[x, y]"

つまり判断は単純です。単発の連結は+、区切り連結はString.join系、ループや条件で積み上げるならStringBuilder、と使い分けます。

StringBuilderで詰まりやすい落とし穴:equals・スレッド共有・文字コード

equalsの参照比較とcompareTo・toStringによる中身の比較

StringBuilderはequalsとhashCodeをオーバーライドしていません。同じ文字列を持つ2つのインスタンスでもequalsはfalseを返し、HashSetのキーにしても中身で重複を判定できません。中身を比べるなら、Java 11で追加されたcompareToが0かどうかを見るか、toString()で確定したString同士を比べます。Javadocにも明記されているのは、「自然順序付けがequalsと一致しない」という比較時の注意点です。文字列比較の基本はJavaのコード例で学ぶ書き方の基本(文字列比較・命名規則)で整理しています。

複数スレッドで1つのStringBuilderを共有したときに起きる文字列の破損

Javadocは「StringBuilderのインスタンスは複数スレッドでの使用に対して安全ではない」と明記しています。並列ストリームや複数の非同期タスクから同じStringBuilderへappendしたときに起きるのは、文字の欠落や混在、まれにArrayIndexOutOfBoundsExceptionが発生する問題です。対処は3つあり、スレッドごとにStringBuilderを持って最後に結合する、ストリームならCollectors.joiningに任せる、どうしても共有するならStringBufferに替える、の順で検討します。

バイト列への変換はStandardCharsets.UTF_8で文字コードを明示する

StringBuilderで組み立てた文字列を、ファイル出力や通信のためにバイト列へ変換する場面では、文字コードを明示します。getBytes()を引数なしで呼ぶと既定の文字コードが使われ、その既定値はJavaの版で変わっています。JEP 400によりJava 18以降は既定がUTF-8になりましたが、Java 17以前の日本語Windowsでは既定がMS932(Shift_JIS系)でした。同じコードが版と環境で別のバイト列を出すため、StandardCharsets.UTF_8のように明示しておけば文字化けの原因を1つ減らせます。

StringBuilder sb = new StringBuilder("こんにちは");
byte[] utf8 = sb.toString().getBytes(StandardCharsets.UTF_8); // 15バイト

StringBuilderのC#・.NET版とJava版の違いと検索時の見分け方

「stringbuilder」で検索すると、Javaのjava.lang.StringBuilderと、C#/.NETのSystem.Text.StringBuilderが混在して表示されます。名前と目的(可変な文字列バッファ)は似ていますが、別言語の別クラスで、メソッド名や挙動は完全には一致しません。たとえば.NET版には改行付きで追加するAppendLineや書式付きのAppendFormatがありますが、Java版にはありません。この記事はJavaを対象にしています。C#で同じことをしたい場合は、Microsoft LearnのSystem.Text.StringBuilderクラスを参照してください。VB.NETやPythonでの「StringBuilder相当」を探している場合も、それぞれの標準ライブラリの仕様に従う必要があります。

StringBuilderを明示する場面・使わない場面の判断と性能の測り方

ここまでの内容を判断の形にまとめます。StringBuilderを明示するのは、(1)ループや再帰で文字列を積み上げる、(2)条件分岐で部分ごとに継ぎ足す、(3)insertやreplaceで途中を書き換える、(4)setLength(0)で同じバッファを使い回したい、のいずれかに当てはまるときです。逆に、1つの式で終わる連結、区切り文字で並べるだけの連結、複数スレッドから同時に書き込む処理では使いません。前の2つは+とString.join系、最後はスレッドごとのバッファかStringBufferの領分です。

性能差を自分で確かめたい場合は、System.nanoTime()の差分ではなく、OpenJDKのJMH(Java Microbenchmark Harness)で測ってください。JITのウォームアップや不要コード除去の影響を受けるため、単純なループ計測では差が出たり消えたりします。測定の前に、本番と同じ件数・同じ文字種でデータを用意することが判断を誤らないコツです。

既存のJavaシステムで、帳票やCSVの生成が遅い・大量データでメモリが溢れるといった症状が出ているなら、文字列処理だけでなく設計全体を見直す時期かもしれません。一創ではフルスクラッチ開発として、Javaを含む業務システムの新規開発と作り直しを支援しています。メモリ不足の切り分けはJavaヒープのメモリ構造とOutOfMemoryErrorの対処も参考になります。

よくある質問

StringBuilderについて、検索でよく調べられている疑問に答えます。

StringBuilderとStringBufferの違いは何ですか?

APIはほぼ同じで、違いはスレッド安全性です。StringBufferは各メソッドがsynchronizedで同期されるためマルチスレッドで共有しても安全ですが、その分わずかに遅くなります。StringBuilderは同期しない代わりに単一スレッドでは高速です。1つのスレッドからしか操作しないなら、StringBuilderを選べば十分です。JavadocもStringBuilderを優先して使うよう推奨しています。

StringBuilderで改行するにはどうしますか?

append("\n")でLF(ラインフィード)を追加できます。実行環境の改行コードに合わせたいならSystem.lineSeparator()をappendします。一方、CSV(RFC 4180ではCRLF)のようにファイル形式で改行コードが決まっている場合は、環境に依存させず"\r\n"などを固定で書くほうが安全です。出力先の仕様に合わせて使い分けてください。

StringBuilderの初期容量(デフォルト)はいくつですか?

引数なしで生成した場合の初期容量は16文字です。文字列を渡して生成した場合の初期容量は、「その文字列の長さ+16」という計算で決まる値です。容量を超えると内部配列が「旧容量の2倍+2」以上に再確保されます。最終的な長さの目安が分かっているなら、new StringBuilder(1024)のように初期容量を指定しておくと、再確保の回数を減らせます。

appendにnullを渡すとどうなりますか?

例外にはならず、文字列"null"(4文字)が末尾に追加されます。append((String) null)もappend((Object) null)も同じ挙動です。意図せず"null"が混入する原因になりやすいので、値がnullになり得る箇所では事前に判定するか、Objects.toString(値, "")で既定値に置き換えてから追加してください。

StringBuilderの中身を空にして再利用するにはどうしますか?

sb.setLength(0)を呼ぶと、確保済みの容量を保ったまま中身だけが空になります。ループの中で毎回new StringBuilder()するより再確保が減るため、同じ処理を何度も繰り返す場面で有効です。sb.delete(0, sb.length())でも同じ結果になります。ただし、一時的に巨大な文字列を入れたバッファは容量も大きいまま残るので、長く保持する場合はtrimToSize()で縮めるか作り直してください。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.07 テックブログ ファインズの不正アクセスと153万件の予約者情報|予約システムを使う店舗の報告と通知の手順
  2. 2025.08.14 コラム トラクションとは?ビジネスでの意味・指標の目安とgain tractionの使い方
  3. 2026.10.15 テックブログ 銀のさら不正アクセス:最大650万件の対象確認と窓口、パスワード変更・退会の手順
  4. 2026.10.19 テックブログ コミックシーモア不正アクセスで会員情報流出:再開時期・パスワード再設定と運営側のログ点検
  5. 2026.10.16 テックブログ バーガーキング情報漏洩どうしたらいい?約1,118万件の対象確認・補償・退会とメールの真偽

RELATED POSTS 関連記事

目次