なぜStringの比較に等価演算子(==)を使ってはいけないの?

| 4 min read
Author: yoshifumi-moriya yoshifumi-moriyaの画像

はじめに

#

日頃、Javaの初心者からベテランまで様々なスキルレベルの方から質問を頂く立場で仕事しておりますが、Java言語を使用するうえで基本事項でありながらも、あまり理解されていない事柄があると感じています。これらの事柄については改めて誰かに質問することもできず、「そういうもの」として無理やり納得しやり過ごしている方がいるのではないでしょうか。ここではそのような事柄について改めて解説し、その理由を知ることで「だらからこうするべき」、「だからこれはダメ」ということを理解する手助けになればと考えます。

今回はStringのequalsと等価演算子(==)について解説します。(次回があるのかは不明)

疑問点

#

ある開発者からこのような質問があったとします。

文字列(String型)の比較には等価演算子(==)ではなくequalsメソッドを使うルールであることは理解している。
しかし、プロダクトコード中にこのルールに違反するものがあるが、単体テストでも特にエラーにはなっていない。
実は等価演算子を使っても実行する上では問題無いが、可読性のためequalsを使うよう統一しているだけなのですか?

私の回答は以下のようになります。

等価演算子やめてすぐにequalsにしてください!ホントにダメです!

==equalsの違い

#

なぜequalsを使わなければならないのかを解説する前に、まずはequals==の違いについて解説します。

等価演算子(==)

#

==は変数の値が等しいかを検証する演算子です。プリミティブ型では値が等しいか(等価性)を検証し、オブジェクト型では同じインスタンスであるか(同一性)を検証します。というと、なぜプリミティブ型とオブジェクト型でこのような違いがあるのか、Javaの言語仕様が分かりにくいのではないか、だからJavaは嫌いなんだ、と憤りを覚える方もいるかもしれません。ですが、実はJavaは同じ「比較演算」をしているに過ぎないのです。

ここで、プリミティブ型の変数とオブジェクト型の変数の違いを考えてみましょう。プリミティブ型の変数には「値」が格納されています。一方でオブジェクト型の変数には、インスタンスへの参照(アドレス)が格納されています。==は変数に格納されているものを比較するので、オブジェクト型では格納されているアドレスを比較することになり、アドレスが等しい、つまり参照先のインスタンスが同一であるかを検証することになるのです。

下の例では、a == b123456が等しいかを検証し、x == yはそれぞれのインスタンスのアドレス(0x7d000x8e00)が等しいかを検証します。

@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のインスタンスは、再利用可能とみなされ同じインスタンスがstr1str2に割り当てられます。つまりstr1str2には同じアドレスが格納されているため==でも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を使う
  • ==の比較がテストでpassしたとしてもそれは「たまたま」であって、使用方法などで変わり得る
  • 当然==の否定である!===と同様の考え方
豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  2. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  3. GitHub Codespaces を使いはじめる (2022-05-18)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  7. Nuxt3入門(第3回) - ユニバーサルフェッチでデータを取得する (2022-10-06)
  8. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第2回) - 簡単なNuxtアプリケーションを作成する (2022-10-02)