equals() 와 ==

mongBrown·2026년 4월 19일

Java == 과 equals() — 같은 값인데 결과가 다른 이유

String a = "hello";
String b = "hello";
System.out.println(a == b);       // true

String c = new String("hello");
String d = new String("hello");
System.out.println(c == d);       // false

같은 "hello"를 담고 있는데, 비교 결과가 다르다. == 는 어떤 기준으로 비교하는 걸까.


== 은 메모리 주소를 비교한다

Java 에서 == 는 두 변수가 같은 메모리 주소를 가리키는지를 본다. 객체의 내용이 같은지는 전혀 따지지 않는다.

String a = "hello";
String b = "hello";

a  →  0x1a2b3c4d  "hello"
b  →  0x1a2b3c4d  "hello"   ← 같은 주소 → a == b : true

---

String c = new String("hello");
String d = new String("hello");

c  →  0x5e6f7a8b  "hello"
d  →  0x9c0d1e2f  "hello"   ← 다른 주소 → c == d : false

new String("hello") 를 두 번 쓰면 JVM 은 Heap 에 별개의 객체를 두 개 만든다. 내용은 같아도 메모리 주소가 다르니 ==false 를 돌려준다.

equals() 는 값을 비교한다

equals() 는 두 객체의 내용이 같은지를 본다. Stringequals() 를 재정의해서 문자 하나하나를 비교한다.

System.out.println(c.equals(d));  // true

이 차이를 흔히 동일성(identity)동등성(equality) 으로 표현한다. == 은 같은 메모리 주소를 가리키는지, equals() 는 내용이 같은지를 본다.

그런데 위 String a == b 비교는 왜 true 였을까. 분명히 두 변수를 따로 선언했는데.

String 리터럴이 재사용되는 이유

"hello" 처럼 따옴표로 직접 쓴 문자열을 리터럴이라고 한다. JVM 은 리터럴을 처리할 때 Heap 안에 따로 관리하는 영역인 String Pool 을 먼저 확인한다. 같은 문자열이 이미 Pool 에 있으면 새 객체를 만들지 않고 기존 것을 재사용한다.

String a = "hello"String b = "hello" 는 둘 다 같은 Pool 객체를 가리키므로 ==true 가 된다.

String 이 불변(immutable)이기 때문에 이 재사용이 안전하다. 한 변수에서 내용을 바꾸면 새 String 이 만들어질 뿐, 다른 변수가 가리키는 Pool 객체는 건드리지 않는다.

new String("hello") 는 명시적으로 새 객체를 요청하는 것이라 Pool 을 무시하고 Heap 에 따로 만든다. 그래서 c == dfalse 다.

String 은 Pool 때문에 예상과 다른 동작을 했다. 숫자 타입에도 비슷한 함정이 있다.

원시 타입(primitive) 과 래퍼 타입(wrapper) 에서 == 이 다르게 동작하는 이유

int, long 같은 원시 타입(primitive)은 객체가 아니다. 값 자체가 스택에 직접 저장되기 때문에 메모리 주소라는 개념이 없다. == 로 비교하면 값을 그대로 비교한다.

int a = 128;
int b = 128;
System.out.println(a == b);  // true — 값 자체를 비교

Integer, Long 같은 래퍼 타입(wrapper)은 다르다. 이들은 객체라서 Heap 에 저장되고, == 는 메모리 주소를 비교한다.

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

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

127 까지는 true, 128 부터는 false 다. JVM 이 자주 쓰이는 작은 정수값을 미리 캐시해두기 때문이다. -128 ~ 127 범위는 캐시된 객체를 재사용하므로 ==true 가 된다. 범위를 넘어서면 새 객체를 만들어서 메모리 주소가 달라진다.

Long 도 같은 범위를 캐시한다. DoubleFloat 은 캐시가 없다. 소수점 값은 가능한 범위가 연속적이라 "자주 쓰이는 값"을 정해 캐시할 수 없기 때문이다.

equals() 를 재정의하지 않으면 어떻게 되는가

equals() 를 재정의하지 않은 클래스에서 equals() 를 호출하면 Object 의 기본 구현이 사용된다.

// Object.equals() 기본 구현
public boolean equals(Object obj) {
    return (this == obj);
}

기본 구현은 == 과 동일하게 참조를 비교한다. 아래처럼 같은 값을 가진 객체를 두 개 만들어도 equals()false 를 돌려준다.

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2));  // false — equals() 미재정의, 참조 비교
System.out.println(p1 == p2);       // false — new 로 만든 서로 다른 객체

p1p2 는 값은 같지만 new 로 생성한 별개의 객체라 메모리 주소가 다르다. 커스텀 클래스에서 값 기반 비교를 하려면 반드시 equals() 를 직접 재정의해야 한다.

객체 비교의 기본은 equals() 다

객체 비교에는 equals() 를 쓰는 것이 기본이다. == 는 두 변수가 정확히 같은 객체를 가리키는지 확인할 때만 의도적으로 사용한다.

String 리터럴이나 Integer 캐시 범위처럼 ==true 로 보이는 경우가 있지만, 이는 JVM 의 최적화 동작이지 값 비교의 결과가 아니다. String 에 new 를 쓰거나 Integer 가 127 을 넘어가는 순간 결과가 달라진다. 값이 같은지 비교하려는 상황이라면 항상 equals() 를 써야 한다.

profile
화이팅!

0개의 댓글