Java : StringBuilderとStringBufferの違い

StringBuilderはスレッドセーフではないけどパフォーマンスが良く、StringBufferはスレッドセーフだけど少しパフォーマンスが悪い。
それぞれのクラスの特徴やおすすめの使い分けかたなど、コード例をまじえて解説していきます。


おすすめの使い分け

初めにおすすめの使い分けを簡単にまとめてしまいます。

  • 基本的にはStringBuilderを使う
  • StringBufferはなるべく使わない
    • 複数のスレッドから文字列を操作したい場合でも、単一スレッドからの操作にできないか?という設計の見直しをしてみる。
      (文字列に対して複数スレッドから更新する、という場面はそんなにないと思うため)
  • それでもダメならStringBufferを使う
    • 明確に複数スレッドから更新することを意図して設計しているのであれば良いと思います。

特徴

このクラスは、StringBufferと互換性があるAPIを提供しますが、同期化は保証されません。
このクラスは、文字列バッファが単一のスレッド(一般的なケース)により使用されていた場合のStringBufferの簡単な代替として使用されるよう設計されています。
このクラスは、ほとんどの実装で高速に実行されるので、可能な場合は、StringBufferよりも優先して使用することをお薦めします。

公式のAPI仕様からの引用です。
単一のスレッドから使う場合(一般的なケース)は、StringBuilderを優先して使うことをおすすめしていますね。

簡単に表にまとめると次のようになります。

クラス スレッドセーフ パフォーマンス
StringBuilder ×
StringBuffer

スレッドセーフについて

StringBufferはスレッドセーフで、StringBuilderはスレッドセーフではありません。
具体例を見てみましょう。

StringBuffer

まずはStringBufferの例です。

final var buffer = new StringBuffer("0123456789");
final var executor = Executors.newFixedThreadPool(20);
try {
    for (int i = 0; i < 101; i++) {
        executor.submit(() -> {
            // 20個のスレッドから合計101回実行されます。
            buffer.reverse();
        });
    }

} finally {
    executor.shutdown();
    executor.awaitTermination(10, TimeUnit.SECONDS);
}

// 101回反転するので、結果として降順となります。
System.out.println(buffer.toString()); // "9876543210"

StringBufferを"0123456789"で作成し、20個のスレッドからreverseメソッドを合計101回呼び出します。

reverseメソッドは文字列を逆順に並び変えるメソッドです。
呼び出すたびに、

"0123456789" → "9876543210" → "0123456789" → "9876543210" → ...

と変化します。
reverseを101回呼び出すと、最終的には降順の"9876543210"になると予想できます。

System.out.println(buffer.toString()); // "9876543210"

期待したとおりの結果となりました。

StringBuilder

次はStringBuilderのコード例です。
StringBufferのところをStringBuilderに置き換えただけのものです。

final var builder = new StringBuilder("0123456789");
final var executor = Executors.newFixedThreadPool(20);
try {
    for (int i = 0; i < 101; i++) {
        executor.submit(() -> {
            builder.reverse();
        });
    }

} finally {
    executor.shutdown();
    executor.awaitTermination(10, TimeUnit.SECONDS);
}

// 意図しない結果になります。(毎回結果が異なります)
System.out.println(builder.toString()); // 0823456710

StringBufferと違い順番がばらばらになりました。
ある意味、予想した結果です。

スレッドの1つがreverseで文字列を並べ替えている途中で、別のスレッドがreverseを呼び出し、その並べ替え中の文字列をさらに並べ替えようとして…という感じでしょうか。

このように、StringBuilderは複数のスレッドから同時に呼び出されると、予期しない結果となります。

補足

StringBufferはスレッドセーフですが、その範囲は1つのメソッドを呼び出している間です。

例えば以下のような複数のメソッド呼び出しの同一性はありません。

final var buffer = new StringBuffer();
final var executor = Executors.newFixedThreadPool(20);
try {
    for (int i = 0; i < 101; i++) {
        executor.submit(() -> {
            buffer.append("abc").append("xyz\n");
        });
    }

} finally {
    executor.shutdown();
    executor.awaitTermination(10, TimeUnit.SECONDS);
}

System.out.println(buffer.toString());
buffer.append("abc").append("xyz\n");

appendメソッドを2回使って、"abcxyz\n"という文字列を追記していきます。

System.out.println(buffer.toString());

結果は…

abcxyz
abcxyz
abcabcxyz
xyz
abcxyz
abcxyz
abcxyz
abcxyz
abcabcxyz
xyz
abcxyz
abcxyz
abcxyz
... 省略 ...

となり、ところどころ衝突しているところがあります。

このように、StringBufferはメソッド単位の同期が行われます。
その点はご注意ください。

パフォーマンス

パフォーマンスはどれくらい違うのか、実際に計測してみました。
appendを100万回呼び出してみます。

StringBuilderのコード例

final var sb = new StringBuilder();
final var start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
    sb.append(i);
}
final var end = System.currentTimeMillis();

System.out.println(end - start); // 約240ミリ秒

StringBufferのコード例

final var sb = new StringBuffer();
final var start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
    sb.append(i);
}
final var end = System.currentTimeMillis();

System.out.println(end - start); // 約360ミリ秒

StringBuilderで約240ミリ秒、StringBufferで約360ミリ秒でした。
(実行環境によっても変わります)

確かにStringBuilderのほうが速いですが、そこまで大きな違いはないようです。
とはいえ、StringBuilderで問題ないときはStringBuilderを使うことをおすすめします。(コードの可読性のためにも)

補足

  • パフォーマンスにそこまで差がないのであれば、安全なStringBufferを常に使った方がよいのでは?と思われるかたもいるかもしれません。
    ただ、それはおすすめしません。
  • 単一スレッドからしか使われない場面でStringBufferを使うと、自分以外の人がコードを読んだときに、これは複数のスレッドからアクセスされる可能性があるのでは?と誤解をあたえます。(つまりコードの可読性が悪くなります)

関連記事

ページの先頭へ