Java / クラスとオブジェクト

String と StringBuilder の比較(参照比較に注意)

結論: 文字列の内容を比べるときは == ではなく equals を使います。String はリテラルが共有されるため == が true になることがありますが、これは参照が同じだっただけです。StringBuilder は equals をオーバーライドしていないので、sb1.equals(sb2) は == と同じ参照比較になり、内容が同じでも false です。StringBuilder 同士は toString() してから equals で比べるか、Java 11 以降なら compareTo を使います。

比較の結果一覧

String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
StringBuilder sb1 = new StringBuilder("abc");
StringBuilder sb2 = new StringBuilder("abc");
式 結果 理由
s1 == s2 true 同じリテラルは文字列プールの同じインスタンスを指す(内容比較ではなく参照が同じだっただけ)
s1 == s3 false new String は必ず新しいインスタンスを作る
s1.equals(s3) true String.equals は内容を比較する
sb1 == sb2 false 別のインスタンス
sb1.equals(sb2) false StringBuilder は Object.equals(参照比較)のまま
sb1.toString().equals(sb2.toString()) true String に変換してから内容を比較
sb1.compareTo(sb2) == 0 true 辞書順比較で差がない(Java 11 以降)
s1.contentEquals(sb1) true String と StringBuilder の内容を直接比較

正しい書き方

if (s1.equals(s3)) { ... }                              // String 同士
if (sb1.toString().equals(sb2.toString())) { ... }      // StringBuilder 同士(どのバージョンでも可)
if (sb1.compareTo(sb2) == 0) { ... }                    // StringBuilder 同士(Java 11 以降)
if ("abc".equals(input)) { ... }                        // input が null でも例外にならない

最後の書き方は、リテラル側から equals を呼ぶことで、input が null のときも NullPointerException にならず false を返します。

原理(なぜそうなるか)

== は参照型に対しては常に「同じインスタンスか」を比べます。String のリテラル("abc" のようにソースに直接書いた文字列)は、同じ内容なら JVM 内の1つのインスタンスに共有されます(インターン)。そのため s1 == s2 は true になりますが、実行時に作られた文字列(new String、ファイルや入力から読んだ値、変数を使った連結など)は別インスタンスなので == は false になります。どちらになるかは文字列の作られ方次第なので、== で内容を比べてはいけません。

String は equals と hashCode を内容に基づいてオーバーライドしています。一方 StringBuilder は内容を後から変更できるクラスで、equals も hashCode もオーバーライドしていません(Object の既定のまま)。内容で比べたいときは、変更できない String に変換して比べるのが基本です。StringBuilder は Java 11 で Comparable<StringBuilder> を実装し、compareTo で内容を辞書順に比べられるようになりました。

(この初版はAIが作成。随時書き換え)

索引へ戻る