こんにちは、かつコーチです。
前回はStringの基本を解説しましたが、大量の文字列を繰り返し連結する処理ではStringの+演算子は非効率です。
この記事では、そうした場面で使うStringBuilder(文字列を効率よく組み立てるためのクラス)の使い方と、なぜ効率が良いのかという理由まで踏み込んで解説します。
StringBuilderとは?
なぜStringの+連結は非効率なのか
Stringはイミュータブル(一度作成すると内容を変更できない性質)なオブジェクトです。+で文字列を連結するたびに、実は新しいStringオブジェクトが毎回生成されています。
String result = "";
for (int i = 0; i < 5; i++) {
result = result + i; // 毎回新しいStringオブジェクトが作られる
}
このコードは一見問題なく動きますが、ループ回数が数千・数万回になると、その都度新しいオブジェクトを生成してはすぐに捨てる、という無駄な処理が積み重なり、パフォーマンスが大きく低下します。
StringBuilderは可変な文字列バッファ
StringBuilderは、内部にバッファ(可変な文字の配列のようなもの)を持ち、そこに直接文字を追加していくクラスです。
新しいオブジェクトを都度生成しないため、大量の連結処理でもメモリと処理時間を節約できます。
基本の書き方:StringBuilderの使い方
append・toStringの基本
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i);
sb.append(",");
}
String result = sb.toString();
System.out.println(result); // 0,1,2,3,4,
}
}
appendメソッドで文字列や数値をどんどん追加し、最後にtoString()で通常のStringに変換します。appendは数値・boolean・char・他のオブジェクトなど、さまざまな型を受け取れるオーバーロードが用意されています。
insert・delete・reverseで柔軟に操作する
StringBuilder sb = new StringBuilder("Hello World");
sb.insert(5, ","); // 指定位置に挿入
System.out.println(sb); // Hello, World
sb.delete(0, 6); // 0〜5文字目を削除
System.out.println(sb); // World
sb.reverse(); // 文字列を反転
System.out.println(sb); // dlroW
Stringにはないinsert(途中への挿入)、delete(範囲削除)、reverse(反転)といったメソッドが使えるのも、可変であるStringBuilderならではの特徴です。
よくあるつまずきポイント・エラー対処
大量ループでStringを使い続けてパフォーマンス劣化に気づかなかった一次情報
私が実際に、CSVのような大きなテキストデータを1万行分組み立てる処理を書いたときの話です。
❌ Before
String csv = "";
for (int i = 0; i < 10000; i++) {
csv = csv + i + "," + (i * 2) + "\n";
}
このコードは動作はするものの、実行にかなりの時間がかかりました。
1万回のループのたびに、それまで組み立てた文字列全体をコピーした新しいStringオブジェクトが生成されるため、処理が進むほど1回あたりのコピー量が増えていきます。
エラーにはならないため気づきにくいのですが、行数が増えるほど処理時間が目に見えて悪化する典型的なパターンでした。
✅ After
StringBuilder csvBuilder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
csvBuilder.append(i).append(",").append(i * 2).append("\n");
}
String csv = csvBuilder.toString();
StringBuilderに書き換えたところ、体感できるほど処理時間が短縮されました。appendは自分自身(StringBuilderのインスタンス)を返すため、.append().append()のようにメソッドチェーン(メソッドを連続して呼び出す書き方)でつなげて書けます。
StringBuilderをそのまま比較してハマるミス
❌ Before
StringBuilder sb1 = new StringBuilder("かつコーチ");
StringBuilder sb2 = new StringBuilder("かつコーチ");
System.out.println(sb1.equals(sb2)); // false
StringBuilderはequalsをオーバーライドしていないため、Stringのように中身を比較してくれません。Objectクラスのequalsがそのまま使われ、結局==と同じ「参照が同じかどうか」の比較になってしまいます。
✅ After
StringBuilder sb1 = new StringBuilder("かつコーチ");
StringBuilder sb2 = new StringBuilder("かつコーチ");
System.out.println(sb1.toString().equals(sb2.toString())); // true
StringBuilder同士の中身を比較したい場合は、toString()でStringに変換してからequalsを使う必要があります。
応用・一歩先の使い方
StringBuilderとStringJoinerの使い分け
import java.util.StringJoiner;
StringJoiner joiner = new StringJoiner(", ", "[", "]");
joiner.add("Java");
joiner.add("Python");
joiner.add("PHP");
System.out.println(joiner.toString()); // [Java, Python, PHP]
区切り文字・前後の括弧付きでリストのような文字列を組み立てたい場合は、StringJoinerの方がコードが簡潔になります。
単純な連結ならStringBuilder、区切り文字付きのリスト表現ならStringJoiner、と目的に応じて使い分けるとよいでしょう。
まとめ
この記事のポイント
- Stringはイミュータブルなため、+演算子での大量連結はパフォーマンスが悪化しやすい
- StringBuilderは可変なバッファを持ち、append中心の操作でメモリ効率よく文字列を組み立てられる
- StringBuilderのequalsは中身を比較しないため、比較するにはtoString()が必要
- 区切り文字付きの連結にはStringJoinerも選択肢に入れる
次に読むべき記事
- Stringの基本:作成・比較・連結
- メソッドの定義と引数の基本
タグ: Java, 中級者向け, 基本文法