JavaのStringは「==」で比較してはいけない実験

Javaをやっている人は当たり前のように文字列の同値比較は`equals`メソッドを使っている。
`==`は参照値の比較つまりインスタンスがメモリ上に入っているアドレスを比較するためだ。

「`==`でも同じかどうか判断されたけどどうして？」と質問されるが、
Javaがメモリ使用量を抑えるために同じ文字列の場合は同じメモリ領域の内容を再利用するため。
ただ、同じ文字列でも再利用される場合とそうでない場合がある。

Java学習のメンバーで実験してみたので、整理して残しておく。

# 前提実験

String以外で異なインスタンスはアドレスが異なることを確認する。

```java
public class EqualsTestObject {

    public static void main(String[] args) {
        System.out.println("----------------------------- 実験1");
        System.out.println("（同じインスタンスなら当然アドレスは同じ）");
        Human human1_1 = new Human("田中一郎");
        Human human1_2 = human1_1;
        System.out.println("アドレス比較\t : " + (human1_1 == human1_2));
        System.out.println("値比較\t\t : " + (human1_1.equals(human1_2)));

        System.out.println();

        System.out.println("----------------------------- 実験2");
        System.out.println("（異なるインスタンスならアドレスは異なる）");
        Human human2_1 = new Human("田中一郎");
        Human human2_2 = new Human("田中一郎");
        System.out.println("アドレス比較\t : " + (human2_1 == human2_2));
        System.out.println("値比較\t\t : " + (human2_1.equals(human2_2)));
    }
}
```

```java
public class Human {
    private String name;
    public Human(String name) {
        this.name = name;
    }

    /** 中身を比較するためequalsメソッドを上書き */
    @Override
    public boolean equals(Object obj) {
        return this.name.equals(((Human)obj).name);
    }
}
```

```console
----------------------------- 実験1
（同じインスタンスなら当然アドレスは同じ）
アドレス比較     : true
値比較           : true

----------------------------- 実験2
（異なるインスタンスならアドレスは異なる）
アドレス比較     : false
値比較           : true
```

このように、`new`で異なるインスタンスを作ると内容物が同じでもアドレスは異なる。

# Stringのインスタンス生成の実験

Stringのインスタンスが再利用されるパターンを確認する。

```java
public class EqualsTestString {

    public static void main(String[] args) {
        System.out.println("----------------------------- 実験A");
        System.out.println("（別々で宣言したのに再利用される）");
        String nameA_1 = "田中一郎";
        String nameA_2 = "田中一郎";
        System.out.println("アドレス比較\t : " + (nameA_1 == nameA_2));
        System.out.println("値比較\t\t : " + (nameA_1.equals(nameA_2)));

        System.out.println();

        System.out.println("----------------------------- 実験B");
        System.out.println("（宣言時に結合しても再利用される）");
        String nameB_1 = "田中一郎";
        String nameB_2 = "田中" + "一郎";
        System.out.println("アドレス比較\t : " + (nameB_1 == nameB_2));
        System.out.println("値比較\t\t : " + (nameB_1.equals(nameB_2)));

        System.out.println();

        System.out.println("----------------------------- 実験C");
        System.out.println("（文字列結合時に新しいインスタンスが生まれて再利用されない）");
        String nameC_1 = "田中一郎";
        String nameC_2 = "田中";
        nameC_2 += "一郎";
        System.out.println("アドレス比較\t : " + (nameC_1 == nameC_2));
        System.out.println("値比較\t\t : " + (nameC_1.equals(nameC_2)));

        System.out.println();

        System.out.println("----------------------------- 実験D");
        System.out.println("（newを使って別々で宣言すると再利用されない）");
        String nameD_1 = new String("田中一郎");
        String nameD_2 = new String("田中一郎");
        System.out.println("アドレス比較\t : " + (nameD_1 == nameD_2));
        System.out.println("値比較\t\t : " + (nameD_1.equals(nameD_2)));

        System.out.println();

        System.out.println("----------------------------- 実験E");
        System.out.println("（StringBuilderで結合後toString()時に新しいインスタンスが生まれて再利用されない）");
        String nameE_1 = "田中一郎";
        StringBuilder nameE = new StringBuilder();
        nameE.append("田中");
        nameE.append("一郎");
        String nameE_2 = nameE.toString();
        System.out.println("アドレス比較\t : " + (nameE_1 == nameE_2));
        System.out.println("値比較\t\t : " + (nameE_1.equals(nameE_2)));
    }
}
```

```console
----------------------------- 実験A
（別々で宣言したのに再利用される）
アドレス比較     : true
値比較           : true

----------------------------- 実験B
（宣言時に結合しても再利用される）
アドレス比較     : true
値比較           : true

----------------------------- 実験C
（文字列結合時に新しいインスタンスが生まれて再利用されない）
アドレス比較     : false
値比較           : true

----------------------------- 実験D
（newを使って別々で宣言すると再利用されない）
アドレス比較     : false
値比較           : true

----------------------------- 実験E
（StringBuilderで結合後toString()時に新しいインスタンスが生まれて再利用されない）
アドレス比較     : false
値比較           : true
```

# 結論

文字列が同じでも`==`では同じと判断されるときと異なると判断されるときがある。
`==`を使っていても同じ文字列の場合に同じと判断される場合があるから間違いにも気づきづらいので要注意！
よって、Javaで文字列の比較を行うときは`equals()`メソッドを使うこと。

# おまけ

比較に`equals()`というインスタンスメソッドを利用するため、
インスタンスがnullの場合NullPointereExceptionの発生するので注意。
比較対象のどちらかが定数もしくはリテラル値の場合は
`equals()`を呼び出すインスタンスを意識するとNullPointereExceptionの発生を回避できる。

```java
public class EqualsTest {

    static final String VALUE = "test";

    public static void test1(String value) {
        // valueがnullでもNullPointetExceptionが発生しない
        if (VALUE.equals(value)) {
            // （省略）
        }
    }

    public static void test2(String value) {
        // valueがnullのときNullPointetExceptionが発生する
        if (value.equals(VALUE)) {
            // （省略）
        }
    }

    public static void main(String[] args) {
        test1(null); // ここではNullPointetExceptionが発生しない
        test2(null); // ここではNullPointetExceptionが発生する
    }
}
```

```console
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "value" is null
        at jp.co.planaria.keongukbackend.beans.EqualsTest.test2(EqualsTest.java:16)
        at jp.co.planaria.keongukbackend.beans.EqualsTest.main(EqualsTest.java:23)
```