24
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

== と equals() の違いを完全に理解する — なぜStringの比較は equals() を使うのか

24
Posted at

はじめに

Javaを書き始めたころ、こんな経験をした方は多いのではないでしょうか。

String a = new String("Hello");
String b = new String("Hello");

if (a == b) {
    System.out.println("同じ");
} else {
    System.out.println("違う"); // ← こっちが出力される!
}

「中身は同じ "Hello" なのに、なぜ "違う" と出力されるの?」

この記事では、==equals() の違いをスタック・ヒープ・文字列プールの仕組みから丁寧に解説します。


==equals() の違いを一言で

== equals()
比較するもの 参照(アドレス) 値(内容)
使いどころ プリミティブ型の比較・null チェック オブジェクトの内容を比較

== は「同じ場所を指しているか」を比較し、equals() は「中身が同じか」を比較します。


理解の前提:スタックとヒープ

Javaのメモリは大きく スタックヒープ の2つに分かれています。

スタック                    ヒープ
(変数名・参照を保持)        (オブジェクトの実体を保持)
[ a ]   ────────────────→   [ "Hello" ]
[ b ]   ────────────────→   [ "Hello" ]
  • スタック:変数名とその値(またはアドレス)を保持する
  • ヒープnew で作られたオブジェクトの実体が保持される

new String("Hello") を2回呼ぶと、ヒープ上に 別々の "Hello" オブジェクトが生成されます。


== はアドレスを比較する

String a = new String("Hello");
String b = new String("Hello");

System.out.println(a == b); // false
スタック             ヒープ
[ a ] → アドレス100 → [ "Hello" ](オブジェクトA)
[ b ] → アドレス200 → [ "Hello" ](オブジェクトB)

ab はそれぞれ別のオブジェクトを指しています。== は「同じアドレスを指しているか」を比較するため、中身が同じでも false になります。


equals() は値(内容)を比較する

String a = new String("Hello");
String b = new String("Hello");

System.out.println(a.equals(b)); // true

String クラスの equals() は、オブジェクトの参照ではなく文字列の内容を比較するようにオーバーライドされています。そのため、中身が同じであれば true が返ります。


文字列リテラルと文字列プール

話を複雑にするのが文字列プールの存在です。

String a = "Hello";  // 文字列リテラル
String b = "Hello";  // 文字列リテラル

System.out.println(a == b); // true !?

new String() を使わずにリテラルで文字列を作ると、Javaは文字列プール(ヒープ内の特別な領域)を参照します。同じ文字列リテラルは使い回されるため、ab同じオブジェクトを指します

スタック             ヒープ(文字列プール)
[ a ] → アドレス100 ─┐
                    ├→ [ "Hello" ](1つだけ)
[ b ] → アドレス100 ─┘

これが a == btrue になる理由です。

new String() との違い

String a = "Hello";             // 文字列プールを参照
String b = new String("Hello"); // ヒープに新しいオブジェクトを生成

System.out.println(a == b);      // false(参照先が違う)
System.out.println(a.equals(b)); // true(中身は同じ)
スタック             ヒープ
[ a ] → アドレス100 → [ "Hello" ](文字列プール)
[ b ] → アドレス200 → [ "Hello" ](新しいオブジェクト)

まとめ:パターン別の比較結果

String a = "Hello";
String b = "Hello";
String c = new String("Hello");
String d = new String("Hello");

// リテラル同士
System.out.println(a == b);       // true(同じプールを参照)
System.out.println(a.equals(b));  // true

// リテラルとnew
System.out.println(a == c);       // false(参照先が違う)
System.out.println(a.equals(c));  // true

// new同士
System.out.println(c == d);       // false(それぞれ別オブジェクト)
System.out.println(c.equals(d));  // true

プリミティブ型は == で比較できる

intboolean などのプリミティブ型はオブジェクトではなく、スタックに値そのものが保持されます。そのため == で値の比較ができます。

int x = 5;
int y = 5;

System.out.println(x == y); // true(値が同じ)
スタック
[ x ] → 5
[ y ] → 5

プリミティブ型には equals() は使えません(オブジェクトではないため)。


ラッパークラスの落とし穴

Integer などのラッパークラスにも注意が必要です。

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false !?

Javaは -128〜127 の範囲の Integer をキャッシュしています(文字列プールと同じ考え方)。そのため127以下は ==true になりますが、128以上は新しいオブジェクトが生成されるため false になります。

ラッパークラスの比較も必ず equals() を使いましょう。

Integer c = 128;
Integer d = 128;
System.out.println(c.equals(d)); // true(安全)

equals() をオーバーライドする

自作クラスでも equals() をオーバーライドすることで、内容による比較ができます。

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;           // 同じ参照なら true
        if (obj == null) return false;           // null なら false
        if (!(obj instanceof User)) return false; // 型が違えば false

        User other = (User) obj;
        return this.age == other.age && this.name.equals(other.name);
    }
}
User user1 = new User("田中", 25);
User user2 = new User("田中", 25);

System.out.println(user1 == user2);       // false(参照が違う)
System.out.println(user1.equals(user2));  // true(内容が同じ)

補足: equals() をオーバーライドするときは、hashCode() も一緒にオーバーライドするのが原則です。HashMapHashSethashCode()equals() を組み合わせて動作するため、片方だけオーバーライドすると予期しない動作になります。


null との比較には注意

equals()null を持つ変数に対して呼び出すと NullPointerException が発生します。

String str = null;
System.out.println(str.equals("Hello")); // NullPointerException!

null チェックが必要な場合は以下の方法が安全です。

// ① 定数側から呼び出す
System.out.println("Hello".equals(str)); // false(例外が出ない)

// ② Objects.equals() を使う(Java 7以降)
System.out.println(Objects.equals(str, "Hello")); // false(null でも安全)

まとめ

比較方法 比較内容 使いどころ
== 参照(アドレス) プリミティブ型・null チェック
equals() 値(内容) オブジェクトの内容を比較
  • String の比較は必ず equals() を使う
  • ラッパークラス(Integer など)も equals() を使う
  • 自作クラスで内容比較したいときは equals() をオーバーライドする
  • null の可能性がある場合は Objects.equals() を使うと安全

==equals() の違いは「参照と値のどちらを比較しているか」という一点に尽きます。スタックとヒープの仕組みを合わせて理解することで、より確かな知識として身につきます。

24
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?