信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
==とequals()はなぜ違うのか — Stringを==で比較してはいけない本当の理由
Javaで==とequals()がなぜ別物なのか。参照比較と値比較の違い、Stringプール、nullとの比較を根本原理から解説する初心者向け記事。
一言結論
==はメモリアドレス(参照)が同じかを比較し、equals()は値が同じかを比較する。Stringリテラルが==で動くように見えるのはStringプールの副作用であり、本質的な比較には必ずequals()を使う。
よくあるバグから始まる
String a = new String("hello");
String b = new String("hello");
if (a == b) {
System.out.println("同じ");
} else {
System.out.println("違う"); // ← こちらが出力される
}
aもbも"hello"なのに「違う」と出力される。初心者が最初につまずくJavaの落とし穴の一つだ。
なぜこうなるのか。「equals()を使えばいい」だけを覚えても、次に同じ間違いをする。仕組みから理解しよう。
==はメモリアドレスの比較だ
Javaの変数には2種類ある:プリミティブ型(int, doubleなど)と参照型(String, 配列, クラスのオブジェクトなど)だ。
プリミティブ型の変数は値そのものを直接格納している:
int x = 42;
┌─────┐
│ 42 │ ← 変数xの中に42という値が入っている
└─────┘
参照型の変数は値そのものではなく、オブジェクトが格納されているメモリアドレス(参照)を格納している:
String a = new String("hello");
メモリのどこか(アドレス 0x100):
┌─────────┐
│ "hello" │ ← Stringオブジェクト本体
└─────────┘
↑
変数a: 0x100 ← aには「0x100番地を見ろ」という情報が入っている
==は「変数の中身がイコールか」を比較する。
- プリミティブ型なら: 値を比較 → 直感通りに動く
- 参照型なら: メモリアドレスを比較 → 「同じ場所を指しているか」を比較
だから:
String a = new String("hello"); // アドレス 0x100 に "hello" を作る
String b = new String("hello"); // アドレス 0x200 に "hello" を作る
a == b // 0x100 == 0x200 → false(違うアドレス)
2つの"hello"は別々のメモリに確保されたオブジェクトだ。中身が同じでも別の場所にある。==はその住所を比較するのでfalseになる。
equals()は中身(値)の比較だ
equals()はオブジェクトの「意味的な等しさ」を比較するメソッドだ。Stringクラスはequals()を「文字列の中身が同じかどうか」で比較するように実装している:
String a = new String("hello");
String b = new String("hello");
a.equals(b) // 文字列の中身を比較 → true
内部では文字列を1文字ずつ比較している。住所は違っても中身が同じならtrueを返す。
なぜリテラル比較では==が「動いてしまう」のか
ここが混乱の原因だ:
String a = "hello";
String b = "hello";
a == b // なぜか true になる
newを使っていない場合、Stringプールという仕組みが働く。
JVMは文字列リテラル("hello"のようなダブルクォートで囲まれた文字列)を特別な場所(Stringプール)にキャッシュする:
Stringプール(特別なメモリ領域):
"hello" → アドレス 0x100
String a = "hello"; → aに 0x100 が入る(プールから取得)
String b = "hello"; → bに 0x100 が入る(同じくプールから取得)
a == b → 0x100 == 0x100 → true
同じ文字列リテラルは同じオブジェクトを指すので、==がtrueになる。
でもこれはStringプールという最適化の副作用であって、文字列の中身を比較しているわけではない。
String a = "hello";
String b = new String("hello"); // newを使うとプールを使わない
a == b // false(プールの0x100 vs 新しく作った0x200)
a.equals(b) // true(中身は同じ)
new String(...)はStringプールを使わず、毎回新しいオブジェクトを作る。
実践的な落とし穴
ユーザー入力や外部から受け取った文字列は必ずnewで作られたようなオブジェクトになる(プールにはない):
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine(); // ユーザーが "yes" と入力
if (input == "yes") { // ❌ 動かない
System.out.println("OK");
}
if (input.equals("yes")) { // ✅ 正しい
System.out.println("OK");
}
==でも一見動くことがあるからこそ厄介だ。テストのときはたまたまプールにある文字列と比較してtrueになり、本番でユーザー入力が来たときだけfalseになる、というバグになりやすい。
nullとの比較に注意する
equals()にはもう一つの落とし穴がある:
String s = null;
s.equals("hello"); // NullPointerException!
nullはオブジェクトではないので、メソッドを呼び出せない。
安全な書き方:
// パターン1: null チェックを先にする
if (s != null && s.equals("hello")) { ... }
// パターン2: 比較する文字列を先に書く(Yoda記法)
if ("hello".equals(s)) { ... } // sがnullでも NullPointerException にならない
パターン2は「hello」というリテラルは絶対nullではないので、"hello".equals(null)はfalseを返すだけでクラッシュしない。
Objectクラスのデフォルトequals()
実は、equals()はObjectクラスで定義されており、すべてのクラスが継承している。デフォルトのequals()は==と同じ動作(参照比較)をする:
// Objectクラスのデフォルト実装(概念的な擬似コード)
public boolean equals(Object obj) {
return (this == obj); // デフォルトは参照比較
}
Stringはこれをオーバーライドして、文字列の中身で比較するように変えている。自作クラスの場合、equals()をオーバーライドしなければデフォルトの参照比較になる:
public class Point {
int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
p1 == p2 // false(別のオブジェクト)
p1.equals(p2) // false(デフォルトのequalsは参照比較なので)
座標が同じなら「等しい」と判定したければ、equals()をオーバーライドする必要がある。
まとめ
== = メモリアドレス(参照)が同じかを比較
equals() = オブジェクトの中身(値)が同じかを比較
Stringプール = リテラル文字列をキャッシュする仕組み。==が偶然trueになる副作用の原因
new String() = Stringプールを使わない。==は必ずfalseになる
ルール:
プリミティブ型(int, double...) → == で比較してOK
参照型(String, オブジェクト) → equals()で比較する
nullかもしれない変数 → "literal".equals(変数) が安全
==が動いているように見えるケースがあるからこそ、気づきにくいバグになる。「参照比較と値比較は別物」という理解が、このクラスのバグを根絶する。