Java String・StringBuilder・StringBufferの違いを比較|使い分け早見表
生徒
「Javaで文字列を扱うときに、String以外にもStringBuilderやStringBufferというものがあると聞きました。これらは何が違うんですか?」
先生
「実はJavaには文字列を扱うクラスが複数あります。一番有名なStringは『一度作ると内容を変えられない』という特徴があるのですが、頻繁に文字を書き換える処理では、StringBuilderやStringBufferを使ったほうが効率が良い場合があるんですよ。」
生徒
「内容を変えられないのに、いつも結合とかできているのはなぜですか?それに、StringBuilderとStringBufferはどう使い分ければいいんでしょうか?」
先生
「鋭いですね!その仕組みやパフォーマンスの差、そして現場での使い分けについて、詳しく解説していきましょう。」
1. Stringクラスの特徴と不変性(Immutable)の仕組み
Javaで最も利用されるStringクラスの最大の特徴は、不変性(イミュータブル)であることです。一度生成された文字列インスタンスの内容は、後から変更することができません。
例えば、文字列を結合する際にプラス演算子を用いることがよくありますが、このとき元の文字列が書き換わっているわけではありません。内部的には、新しい文字列メモリ領域が確保され、結合後の新しいインスタンスが作成されています。そのため、ループの中で何万回も文字列を結合すると、その都度新しいオブジェクトが生成され、メモリ消費量が増大し、処理速度が低下する原因になります。
初心者のうちはあまり意識しませんが、大規模な開発や大量のデータ処理を行う際には、このStringの性質がパフォーマンスのボトルネックになることを覚えておきましょう。まずは、基本的なStringの挙動をコードで確認します。
public class StringExample {
public static void main(String[] args) {
String message = "Java";
// 文字列を結合しているように見えるが、新しいインスタンスが作られている
message = message + "プログラミング";
System.out.println(message);
}
}
Javaプログラミング
2. StringBuilderクラスとは?高速な文字列操作のメリット
StringBuilderクラスは、Stringとは対照的に可変(ミュータブル)なクラスです。文字列の追加や挿入、削除を行う際に、新しいオブジェクトを作らずに同じメモリ領域内で内容を書き換えます。
このため、ループ処理の中で文字列を組み立てる場合には、StringBuilderを使うのがJavaプログラミングの鉄則です。内部のバッファと呼ばれる領域を効率的に利用することで、メモリの節約と実行速度の向上を両立させています。特に、数千回から数万回の文字列結合を行う場合、Stringと比較して数百倍以上の速度差が出ることも珍しくありません。
また、StringBuilderは後述するStringBufferよりも高速に動作します。これは「スレッドセーフ」という機能をあえて持たせていないため、余計なオーバーヘッドが発生しないからです。個人で学習するプログラムや、一般的なWebアプリケーションのほとんどの場面では、このStringBuilderが推奨されます。
public class StringBuilderExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("こんにちは");
// appendメソッドで文字列を追加していく
sb.append("、今日は").append("晴れですね。");
// 一部を書き換えることも可能
sb.replace(0, 5, "ハロー");
System.out.println(sb.toString());
}
}
ハロー、今日は晴れですね。
3. StringBufferクラスの役割とスレッドセーフの概念
StringBufferクラスは、StringBuilderと非常によく似た機能を持ちますが、決定的な違いはスレッドセーフ(同期化)であるという点です。これは、複数の処理(スレッド)が同時に同じStringBufferオブジェクトにアクセスしても、データの整合性が保たれる仕組みのことです。
Javaの初期のバージョンから存在するクラスであり、昔の教科書や古いシステムでは頻繁に見かけます。しかし、現代のプログラミングにおいて、一つの文字列操作オブジェクトを複数のスレッドで共有することは稀です。そのため、基本的にはStringBuilderにその座を譲っています。
もし、マルチスレッド環境において、複数の場所から同時に文字列を書き換える必要がある特殊なケースであればStringBufferを選択しますが、それ以外の状況で敢えて選ぶ必要はありません。StringBuilderと使い方は全く同じなので、知識として「安全性が高いが少し遅い兄貴分」のようなイメージで持っておくと良いでしょう。
4. String・StringBuilder・StringBufferの使い分け早見表
それぞれのクラスの特徴を整理しました。どのクラスを使うべきか迷ったときは、この表を参考にしてください。
| 項目 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 変更可能性 | 不変(Immutable) | 可変(Mutable) | 可変(Mutable) |
| 速度 | 遅い(結合時) | 非常に高速 | 高速(同期化のため微減) |
| スレッド安全 | 安全 | 安全ではない | 安全(スレッドセーフ) |
| 主な用途 | 定数、短い文字列 | 大量の文字列結合 | マルチスレッド環境 |
初心者がコードを書く際の判断基準はシンプルです。「普通の文字列ならString」、「for文などで何度も文字をくっつけるならStringBuilder」と覚えましょう。StringBufferの出番は現代では非常に限定的です。
5. パフォーマンスの違いを体感する!具体的な処理速度の比較
なぜStringBuilderが必要なのか、実際の速度差をイメージしてみましょう。例えば、10万回の文字列結合を行う場合を想定します。
Stringで行うと、内部で10万個の新しいオブジェクトが作成され、古いオブジェクトはゴミ(ガベージ)としてメモリに溜まっていきます。これを掃除するガベージコレクションの負荷も高まり、プログラムが数秒から数十秒フリーズすることもあります。対して、StringBuilderであれば、最初に用意した器の中に文字を流し込むだけなので、ミリ秒単位で処理が完了します。
このように、データ量が増えた際の影響を考えることは、プロのエンジニアを目指す上で非常に重要です。以下のサンプルコードでは、実際にループを使ってStringBuilderで効率的に文字列を生成する例を示します。
public class PerformanceDemo {
public static void main(String[] args) {
StringBuilder data = new StringBuilder();
String[] words = {"りんご", "みかん", "バナナ", "ぶどう"};
for (int i = 0; i < 100; i++) {
for (String word : words) {
data.append(word).append(",");
}
}
// 最後に不要なカンマを削除
if (data.length() > 0) {
data.setLength(data.length() - 1);
}
System.out.println("生成された文字数: " + data.length());
}
}
生成された文字数: 1299
6. 便利なメソッド紹介!append以外の活用術
StringBuilderやStringBufferには、文字列操作をさらに便利にするためのメソッドが用意されています。これらを使いこなすことで、煩雑な文字列処理をスッキリと記述できるようになります。
- insert(index, str): 指定した位置に文字列を差し込みます。
- delete(start, end): 指定した範囲の文字列を削除します。
- reverse(): 文字列の並びを反転させます。
- setCharAt(index, char): 特定の位置の1文字だけを別の文字に変更します。
これらのメソッドはStringクラスには存在しません。Stringで同じことをしようとすると、substringで分割して結合し直すという手間が発生しますが、StringBuilderなら一発で解決します。特にreverseメソッドは、回文のチェックやデータの加工などで地味に役立つメソッドです。
public class AdvancedMethods {
public static void main(String[] args) {
StringBuilder result = new StringBuilder("Java Framework");
// 5文字目に「Spring 」を挿入
result.insert(5, "Spring ");
System.out.println("挿入後: " + result);
// 文字列を逆転させる
result.reverse();
System.out.println("反転後: " + result);
}
}
挿入後: Java Spring Framework
反転後: krowemarF gnirpS avaJ
7. 現場で役立つ注意点とメモリ管理のポイント
StringBuilderを使う際に知っておくと得する知識が「初期容量(Capacity)」です。StringBuilderは内部に配列を持っており、デフォルトでは16文字分程度の空き地を確保しています。もし、追加する文字がこのサイズを超えると、自動的に大きな配列を確保し直して中身をコピーします。
あらかじめ「非常に長い文字列(数万文字など)を扱う」ことが分かっている場合は、コンストラクタで初期サイズを指定することが可能です。これにより、配列の再確保という無駄な処理を減らすことができ、さらなる高速化が望めます。
また、最終的にStringとして出力や保存をしたい場合は、必ずtoString()メソッドを呼び出してString型に変換しましょう。Javaの多くのAPIは引数としてString型を求めているため、この変換処理は頻繁に行うことになります。逆に言えば、加工が終わるまではずっとStringBuilderのまま持ち回るのがベストプラクティスです。
8. Javaの最新バージョンにおける文字列結合の裏側
実は、近年のJava(Java 8以降やJava 11以降など)では、単純なStringのプラス結合をコンパイラが自動的に最適化してくれる場合があります。例えば "a" + "b" + "c" と書くと、内部的にStringBuilderを使って効率よく結合されるように変換されるのです。
「じゃあ、全部Stringでいいの?」と思うかもしれませんが、それはNOです。ループ文の中での結合(特にfor文やwhile文の中で変数を更新していく場合)は、コンパイラによる自動最適化が効きにくい領域です。ループごとに新しいStringBuilderオブジェクトが作られては消えるという非効率なコードに変換されてしまうことが多いため、繰り返し処理の中では明示的にStringBuilderを宣言して使い回す必要があります。
エンジニアとして「なぜこのコードを書くのか」という根拠を持つためには、こうした裏側の仕組みを理解しておくことが欠かせません。基本はStringを使い、ループや複雑な加工が必要なときだけStringBuilderに頼るという姿勢が、スマートなプログラミングへの第一歩です。