Java : StringBuilderとStringBufferの違い
StringBuilder はスレッドセーフではないけれどパフォーマンスがよく、StringBuffer はスレッドセーフだけど少しパフォーマンスが悪いです。
本記事では、それぞれのクラスの特徴やおすすめの使い分けなど、コード例をまじえて解説していきます。
おすすめの使い分け
初めにおすすめの使い分けを簡単にまとめてしまいます。
- 基本的には StringBuilder を使う
- StringBuffer はなるべく使わない
- 複数のスレッドを使っている場合でも、単一スレッドからのみの操作に変更できないか?という設計の見直しをしてみる。
(文字列に対して複数スレッドから更新したい、という場面はそんなにないと思うため)
- 複数のスレッドを使っている場合でも、単一スレッドからのみの操作に変更できないか?という設計の見直しをしてみる。
- それでもダメなら StringBuffer を使う
- 明確に複数スレッドから更新することを意図して設計しているのであれば良いと思います。
それぞれの特徴
StringBuilder の公式API仕様からの引用です。
単一のスレッドから使う場合(一般的なケース)は、StringBuilder を優先して使うことをおすすめしていますね。
簡単に表にまとめると次のようになります。
クラス | スレッドセーフ | パフォーマンス |
---|---|---|
StringBuilder | × | ○ |
StringBuffer | ○ | △ |
スレッドセーフについて
StringBuffer はスレッドセーフで、StringBuilder はスレッドセーフではありません。
具体例を見てみましょう。
StringBuffer
まずは StringBuffer の例です。
final var buffer = new StringBuffer("0123456789");
try (final var executor = Executors.newFixedThreadPool(20)) {
for (int i = 0; i < 101; i++) {
executor.submit(() -> {
// 20個のスレッドから合計101回実行されます。
buffer.reverse();
});
}
}
// 101回反転するので、結果として降順となります。
System.out.println(buffer); // "9876543210"
StringBuffer を "0123456789" で作成し、20個のスレッドから reverseメソッドを合計101回呼び出します。
reverseメソッドは文字列を逆順に並び変えるメソッドです。
呼び出すたびに、
"0123456789" → "9876543210" → "0123456789" → "9876543210" → ...
と変化します。
reverse を101回呼び出すと、最終的には降順の "9876543210" になると予想できます。
System.out.println(buffer); // "9876543210"
期待したとおりの結果となりました。
StringBuilder
次は StringBuilder のコード例です。
StringBuffer のところを StringBuilder に置き換えただけのものです。
final var builder = new StringBuilder("0123456789");
try (final var executor = Executors.newFixedThreadPool(20)) {
for (int i = 0; i < 101; i++) {
executor.submit(() -> {
builder.reverse();
});
}
}
// 意図しない結果になります。(毎回結果が異なります)
System.out.println(builder); // "0823456710"
StringBuffer と違い順番がばらばらになりました。
ある意味、予想した結果です。
スレッドの1つが reverse で文字列を並べ替えている途中で、別のスレッドが reverse を呼び出し、その並べ替え中の文字列をさらに並べ替えようとして…という感じでしょうか。
このように、StringBuilder は複数のスレッドから同時に呼び出されると、予期しない結果となります。
補足
StringBuffer はスレッドセーフですが、その範囲は1つのメソッドを呼び出している間です。
例えば次のような複数のメソッド呼び出しの同一性はありません。
final var buffer = new StringBuffer();
try (final var executor = Executors.newFixedThreadPool(20)) {
for (int i = 0; i < 101; i++) {
executor.submit(() -> {
buffer.append("abc").append("xyz\n");
});
}
}
System.out.println(buffer);
この例では、appendメソッドを2回使って、"abcxyz\n"という文字列を追記していきます。
buffer.append("abc").append("xyz\n");
出力される結果は…
System.out.println(buffer);
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.nanoTime();
for (int i = 0; i < 10000000; i++) {
sb.append(i);
}
final var end = System.nanoTime();
System.out.println((end - start) / 1000000000.0 + " sec."); // 0.2959844 sec.
StringBuffer のコード例
final var sb = new StringBuffer();
final var start = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
sb.append(i);
}
final var end = System.nanoTime();
System.out.println((end - start) / 1000000000.0 + " sec."); // 0.3964447 sec.
StringBuilder で約0.29秒、StringBuffer で約0.39秒でした。
(実行環境によっても変わります)
確かに StringBuilder のほうが速いですが、そこまで大きな違いはないようです。
とはいえ、StringBuilder で問題ないときは StringBuilder を使うことをおすすめします。(コードの可読性のためにも)
補足
- パフォーマンスにそこまで差がないのであれば、安全な StringBuffer を常に使った方がよいのでは?と思われるかたもいるかもしれません。
ただ、それはあまりおすすめしません。 - 単一スレッドからしか使われない場面で StringBuffer を使うと、自分以外の人がコードを読んだときに、これは複数のスレッドからアクセスされる可能性があるのでは?と誤解をあたえます。(つまりコードの可読性が悪くなります)
まとめ
- 基本的にはパフォーマンスのよい StringBuilder を使いましょう。
- 複数スレッドから更新することを意図して設計しているときのみ StringBuffer を使いましょう。