結論: 文字列の内容を比べるときは == ではなく 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が作成。随時書き換え)