AIによる要約
StringBuilderは内容が同じでもequalsでtrueになりません。StringBuilderはObjectのequalsを内容比較用にオーバーライドしていないため、同じインスタンスかを比較します。内容を比較するならtoStringしてString.equalsを使うか、String側のcontentEqualsへStringBuilderを渡します。StringBuilderは可変なので、Setの重複判定やMapのキーにも向きません。比較する時点で不変なStringへ変換するのが基本です。


StringBuilderはループで文字列を組み立てる時や、CSV・SQL・メッセージを生成する時に使います。作り終えた内容を期待値と比較したところ、見た目が同じなのにfalseになり、テストで迷うことがあります。
原因は`StringBuilder.equals`の実装です。`==`と`equals`の一般論だけでなく、対象クラスがequalsをどう定義しているか確認する必要があります。
この記事のポイント
- StringBuilderのequalsは内容比較にならない
- 同じ内容でも別インスタンスならfalseになる
- 内容比較はtoStringしてString.equalsを使える
- String.contentEqualsでもStringBuilderと比較できる
- 可変なStringBuilderをMap・Setのキーにしない
XのJavaクイズで結果を確認する

StringBuilder a = new StringBuilder("Java");
StringBuilder b = new StringBuilder("Java");
System.out.println(a.equals(b));結果はfalseです。aとbは内容が同じでも別インスタンスだからです。

equalsは必ず内容比較するメソッドではない
すべてのクラスはObjectを継承します。Objectのequalsは基本的に同じインスタンスかを判定します。Stringはequalsをオーバーライドし、文字列内容を比較するようにしています。
String a = new String("Java");
String b = new String("Java");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // trueStringBuilderは内容を変更できる可変クラスです。内容比較用にequalsをオーバーライドしていないため、`a.equals(b)`でも参照の同一性を見る結果になります。
StringBuilder a = new StringBuilder("Java");
StringBuilder same = a;
System.out.println(a.equals(same)); // true同じインスタンスを指す`same`ならtrueです。これで、内容ではなくインスタンスを見ていることが分かります。
toStringしてStringとして比較する
StringBuilder a = new StringBuilder("Java");
StringBuilder b = new StringBuilder("Java");
boolean sameContent =
a.toString().equals(b.toString());
System.out.println(sameContent); // true最も分かりやすい方法です。比較時点の内容をStringへ変換し、String.equalsで比較します。JUnitの期待値もStringにしておくと失敗メッセージを読みやすくできます。
StringBuilder actual = messageBuilder.build(order);
assertEquals(
"注文を受け付けました",
actual.toString());String.contentEqualsで比較する
String expected = "Java";
StringBuilder actual = new StringBuilder("Java");
boolean same =
expected.contentEquals(actual);Stringの`contentEquals(CharSequence)`は、StringBuilderなどのCharSequenceと内容を比較できます。比較対象の片方がStringである場合に、`toString`を明示せず書けます。
向きを逆にして`actual.equals(expected)`とするとfalseになるため、チームで見慣れていない場合はtoStringを使う方が誤解を減らせます。短さより、比較方法が伝わることを優先します。
compareToが使えてもequalsとは別物
StringBuilder a = new StringBuilder("Java");
StringBuilder b = new StringBuilder("Java");
System.out.println(a.compareTo(b) == 0); // true
System.out.println(a.equals(b)); // false利用するJavaバージョンではStringBuilderのcompareToで辞書順比較ができます。しかしcompareToが0でもequalsがtrueとは限りません。`SortedSet`やソート処理では、この不一致が読み手を混乱させるため、文字列値として扱うならStringへ変換する方が自然です。
StringBuilderをMapやSetのキーにしない
Set<StringBuilder> values = new HashSet<>();
values.add(new StringBuilder("Java"));
values.add(new StringBuilder("Java"));
System.out.println(values.size()); // 2内容が同じでもequalsがtrueにならないため、Setは重複として扱いません。Mapでも、同じ内容の別StringBuilderを使って検索しても見つかりません。
Map<String, Integer> counts = new HashMap<>();
StringBuilder builder = new StringBuilder("Java");
counts.put(builder.toString(), 1);
System.out.println(counts.get("Java")); // 1キーとして保存する時点でStringへ確定します。Stringは不変なので、登録後に内容が変わる心配もありません。
可変オブジェクトをキーにする設計を避ける
StringBuilder以外でも、equalsやhashCodeに使う項目を登録後に変更すると、HashMapやHashSetから見つけにくくなります。キーはString、ID、変更しない値オブジェクトなどを使います。
nullを含む比較ではObjects.equalsを使う
StringBuilder自体がnullかもしれない場合、すぐ`toString`するとNullPointerExceptionになります。
String actual = builder == null
? null
: builder.toString();
boolean same =
Objects.equals(expected, actual);ただし「builderがnullでも空文字として扱う」のか「異常とする」のかは仕様です。安易に空文字へ変換すると、値未取得と空文字入力を同じにしてしまいます。
現場でよくある比較ミス
- CSVの1行をStringBuilderで作り、期待値Stringとequalsしてfalseになる
- StringBuilderをSetへ入れて重複削除できない
- SQL文字列のテストでStringBuilder同士をassertEqualsする
- キャッシュキーをStringBuilderのままMapへ登録する
- toString前のnullを考慮せずNullPointerExceptionになる
StringBuilderは作成途中の器です。業務上の値として比較、保存、送信する境界ではStringへ変換します。「いつStringへ確定するか」を決めると、比較ミスを減らせます。
テストで別インスタンスを使う
@Test
void StringBuilderのequalsは内容比較しない() {
StringBuilder a = new StringBuilder("Java");
StringBuilder b = new StringBuilder("Java");
assertNotEquals(a, b);
assertEquals(a.toString(), b.toString());
}
@Test
void StringのcontentEqualsで比較できる() {
String expected = "Java";
StringBuilder actual =
new StringBuilder("Java");
assertTrue(expected.contentEquals(actual));
}同じインスタンスを期待値と実測値へ渡すと、参照比較でもtrueになり問題を見逃します。内容が同じ別インスタンスを作り、比較したいものが参照か内容かを明確にします。
現場レビューでよくある指摘
// レビューコメント例
StringBuilderのequalsは文字列内容を比較しないため、
toStringしてString.equalsで比較してください。
// レビューコメント例
StringBuilderをHashMapのキーにすると、
同じ内容の別インスタンスから検索できません。Stringへ確定してください。
// レビューコメント例
builderがnullの場合にtoStringで例外になります。
nullを異常とするか、値なしとして扱うか仕様を明示してください。提出前のセルフチェック
- equalsが対象クラスでどう実装されているか確認したか
- 比較したいのは参照か内容か
- 内容比較ならStringへ変換したか
- String.contentEqualsの向きを間違えていないか
- StringBuilderをMap・Setのキーにしていないか
- 比較時点でnullの可能性を確認したか
- 別インスタンスを使ってテストしたか
- 値を確定する境界が明確か
Javaの文字列とequalsを整理する参考書
String、StringBuilder、Objectのequalsを基礎から学ぶと、API名だけで比較方法を決めるミスを防げます。
スッキリわかるJava入門 第5版
Java文法とオブジェクト指向を、最初から整理する一冊。
豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。
- Javaを基礎から体系的に学びたい
- Java 25 LTS対応の入門書を選びたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
StringBuilderは内容比較用にequalsをオーバーライドしていないため、同じ文字を持つ別インスタンスを比較してもfalseになります。
内容を比較するならtoStringしてString.equalsを使うか、String.contentEqualsへ渡します。StringBuilderは文字列を組み立てる途中の器として使い、比較・保存・Mapキーの境界では不変なStringへ確定しましょう。
