はじめに
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)
a と b はそれぞれ別のオブジェクトを指しています。== は「同じアドレスを指しているか」を比較するため、中身が同じでも 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は文字列プール(ヒープ内の特別な領域)を参照します。同じ文字列リテラルは使い回されるため、a と b は同じオブジェクトを指します。
スタック ヒープ(文字列プール)
[ a ] → アドレス100 ─┐
├→ [ "Hello" ](1つだけ)
[ b ] → アドレス100 ─┘
これが a == b が true になる理由です。
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
プリミティブ型は == で比較できる
int や boolean などのプリミティブ型はオブジェクトではなく、スタックに値そのものが保持されます。そのため == で値の比較ができます。
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()も一緒にオーバーライドするのが原則です。HashMapやHashSetはhashCode()と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() の違いは「参照と値のどちらを比較しているか」という一点に尽きます。スタックとヒープの仕組みを合わせて理解することで、より確かな知識として身につきます。