なぜStringの比較に等価演算子(==)を使ってはいけないの?
はじめに
#日頃、Javaの初心者からベテランまで様々なスキルレベルの方から質問を頂く立場で仕事しておりますが、Java言語を使用するうえで基本事項でありながらも、あまり理解されていない事柄があると感じています。これらの事柄については改めて誰かに質問することもできず、「そういうもの」として無理やり納得しやり過ごしている方がいるのではないでしょうか。ここではそのような事柄について改めて解説し、その理由を知ることで「だらからこうするべき」、「だからこれはダメ」ということを理解する手助けになればと考えます。
今回はStringのequals
と等価演算子(==
)について解説します。(次回があるのかは不明)
疑問点
#ある開発者からこのような質問があったとします。
文字列(String型)の比較には等価演算子(
==
)ではなくequals
メソッドを使うルールであることは理解している。
しかし、プロダクトコード中にこのルールに違反するものがあるが、単体テストでも特にエラーにはなっていない。
実は等価演算子を使っても実行する上では問題無いが、可読性のためequals
を使うよう統一しているだけなのですか?
私の回答は以下のようになります。
等価演算子やめてすぐに
equals
にしてください!ホントにダメです!
==
とequals
の違い
#なぜequals
を使わなければならないのかを解説する前に、まずはequals
と==
の違いについて解説します。
等価演算子(==
)
#==
は変数の値が等しいかを検証する演算子です。プリミティブ型では値が等しいか(等価性)を検証し、オブジェクト型では同じインスタンスであるか(同一性)を検証します。というと、なぜプリミティブ型とオブジェクト型でこのような違いがあるのか、Javaの言語仕様が分かりにくいのではないか、だからJavaは嫌いなんだ、と憤りを覚える方もいるかもしれません。ですが、実はJavaは同じ「比較演算」をしているに過ぎないのです。
ここで、プリミティブ型の変数とオブジェクト型の変数の違いを考えてみましょう。プリミティブ型の変数には「値」が格納されています。一方でオブジェクト型の変数には、インスタンスへの参照(アドレス)が格納されています。==
は変数に格納されているものを比較するので、オブジェクト型では格納されているアドレスを比較することになり、アドレスが等しい、つまり参照先のインスタンスが同一であるかを検証することになるのです。
下の例では、a == b
は123
と456
が等しいかを検証し、x == y
はそれぞれのインスタンスのアドレス(0x7d00
と0x8e00
)が等しいかを検証します。
@Test
void testEqualOperator() {
int a = 123;
int b = 456;
assertEquals(false, a == b);
LocalDate x = LocalDate.of(2022, 1, 23);
LocalDate y = LocalDate.of(2022, 1, 24);
assertEquals(false, x == y);
}
equals
メソッド
#equals
メソッドはインスタンスが等価であるかを検証するメソッドです。==
ではインスタンスの値が等しいかまでは検証できないためObjectクラスに設けられたメソッドです(おそらく)。なお、インスタンスがどのような場合に「等価である」とするのかはそのクラスのequals
メソッドの実装により決定します。ちなみにObjectクラスの実装は==
での比較となっています。
例えば日付を表すLocalDate型では以下のようなテストコードを書いた場合、同一の日付でも==
では「等しくない」(同ーインスタンスではない)、equals
では「等しい」(日付の値が同じ)となります。
@Test
void testLocalDate() {
LocalDate date1 = LocalDate.of(2022, 1, 23);
LocalDate date2 = LocalDate.of(2022, 1, 23);
assertEquals(true, date1.equals(date2y)); // 同値なのでpass(テスト成功)
assertEquals(true, date1 == date2); // 同一インスタンスではないのでfail(テスト失敗)
}
Stringの時の挙動
#Stringの場合の挙動はどうなるでしょうか。Stringは使い方がプリミティブ型に似ている(new
でインスタンスを作る必要がない)ため誤解されがちですが、オブジェクト型です。従ってStringの比較にはequals
を使用するのが正しい、となります。
まず、以下のテストコードはどうなるでしょうか。
@Test
void testString() {
String str1 = "Abcd";
String str2 = "Abcd";
assertEquals(true, str1.equals(str2));
assertEquals(true, str1 == str2);
}
当然equals
での比較はpassするのですが、このケースの場合==
での比較もpassしてしまうのです。これは何故でしょうか。
Javaでは使用するメモリを節約するためStringのインスタンスは可能な場合、再利用されます。上記例では"Abcd"
というStringのインスタンスは、再利用可能とみなされ同じインスタンスがstr1
、str2
に割り当てられます。つまりstr1
、str2
には同じアドレスが格納されているため==
でもtrue
という結果になります。
次に、こちらのテストコードではどうなるでしょうか。
@Test
void testString2() {
String str1 = "Abcd";
String str2 = new String("Abcd");
assertEquals(true, str1.equals(str2));
assertEquals(true, str1 == str2); // 別インスタンスなのでfail
}
こちらの場合、str2
にはnew String
を使うことで「新しいStringのインスタンスを作成」することを明示しているため、値は同じでもstr1
とは異なるインスタンスが作成されます。従って、==
ではfailします。
では、どのような時にStringが再利用されるのでしょうか?それはJavaの実装によりますので、はっきり言ってわかりません。テストでは問題が無くても商用環境では障害になってしまう、ということが起こり得ます。なので、Stringはequals
で比較しなければならないのです。
改めて結論
#- 障害になり得るので、Stringの比較は
equals
メソッドで行う- Stringに限らず、オブジェクト型は特別な理由(インスタンスの同一性を検証したい)がない限り
equals
を使う
- Stringに限らず、オブジェクト型は特別な理由(インスタンスの同一性を検証したい)がない限り
==
の比較がテストでpassしたとしてもそれは「たまたま」であって、使用方法などで変わり得る- 当然
==
の否定である!=
も==
と同様の考え方