同じ文字を持つStringBuilderをequalsで比較してもfalseになる理由を解説する記事のアイキャッチ。

Java

JavaのStringBuilderでequalsがfalseになる理由|文字列の内容を比較する方法

AIによる要約

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

新人SE
新人SE
Stringはequalsで内容比較できたので、StringBuilderも同じだと思っていました。
ポンコツSE
ポンコツSE
equalsの意味はクラスごとに決まります。StringBuilderは内容比較用のequalsを持たないため、Objectと同じ参照比較です。

StringBuilderはループで文字列を組み立てる時や、CSV・SQL・メッセージを生成する時に使います。作り終えた内容を期待値と比較したところ、見た目が同じなのにfalseになり、テストで迷うことがあります。

原因は`StringBuilder.equals`の実装です。`==`と`equals`の一般論だけでなく、対象クラスがequalsをどう定義しているか確認する必要があります。

この記事のポイント

  • StringBuilderのequalsは内容比較にならない
  • 同じ内容でも別インスタンスならfalseになる
  • 内容比較はtoStringしてString.equalsを使える
  • String.contentEqualsでもStringBuilderと比較できる
  • 可変なStringBuilderをMap・Setのキーにしない

XのJavaクイズで結果を確認する

同じJavaという内容を持つ2つのStringBuilderをequalsで比較した結果を問うJavaクイズ画像。
内容は同じですが、別々にnewしたStringBuilderです。
StringBuilder a = new StringBuilder("Java");
StringBuilder b = new StringBuilder("Java");

System.out.println(a.equals(b));

結果はfalseです。aとbは内容が同じでも別インスタンスだからです。

StringBuilderはequalsをオーバーライドしておらず、同じインスタンスかを比較するためfalseになると説明する回答画像。
StringBuilderのequalsは、文字列内容の一致を判定しません。

Xの出題ポスト回答ポストも参照できます。

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)); // true

StringBuilderは内容を変更できる可変クラスです。内容比較用に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版」の表紙
商品画像:Amazon.co.jp
PR Java初学者向け

スッキリわかるJava入門 第5版

中山 清喬 (著), 国本 大悟 (著), 株式会社フレアリンク (監修)

Java文法とオブジェクト指向を、最初から整理する一冊。

豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。

  • Javaを基礎から体系的に学びたい
  • Java 25 LTS対応の入門書を選びたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

StringBuilderは内容比較用にequalsをオーバーライドしていないため、同じ文字を持つ別インスタンスを比較してもfalseになります。

内容を比較するならtoStringしてString.equalsを使うか、String.contentEqualsへ渡します。StringBuilderは文字列を組み立てる途中の器として使い、比較・保存・Mapキーの境界では不変なStringへ確定しましょう。

-Java
-, , , ,