본문 바로가기

개발 공부

.equals(), == 비교

면접에서 equals와 ==의 차이점에 대해 설명해달라는 질문을 받은 나는 그만 벙쪄버렸다.

분명 알고 있는 개념이라고 생각했는데 막상 설명하려고 하니 말문이 턱 막혀버린 것이다. 이처럼 알고 있다고 생각하는 개념들이 생각보다 많은데 앞으로 직접 설명해보면서 모르는 것 같은 개념들에 대해 다시 정리해보고자 한다.

 

첫번째는 가장 최근에 질문 받았던 .equals와 == 의 차이점에 대해 알아보고자 한다. 원래 알고 있던 내용으로 설명해보자면 .equals 는 String 끼리의 비교에서 사용되고 == 은 값 자체를 비교하는 것이라 다른 것들을 비교할 때 사용된다는 느낌으로 알고 있었는데, 이처럼 실제로 말로 해보니 전혀 개념에 대해 제대로 파악하고 있지 않다는 것을 알았다. 

 

그럼 이제 실제 .equals와 == 이 어떤 식으로 비교하고 어떻게 결과가 나오는지 알아보도록 하겠다.

✅ 1. == (동일성 비교, Reference 비교)

  • 객체 레퍼런스(참조) 주소가 같은지를 비교한다.
    두 변수가 같은 인스턴스를 가리키는가? 를 묻는다.
  • 내부적으로는 JVM 레퍼런스(포인터/핸들) 비교다. 메모리 주소가 동일한지 확인한다는 개념이 맞지만, GC가 객체를 이동할 수 있으므로 "물리적 주소"에 집착할 필요는 없음. 결과적으로는 동일 객체(identity) 인지 확인.
  • 원시 타입(primitive)일 경우 → 값 비교
 
String a = "hello"; 
String b = "hello"; 
String c = new String("hello"); 
System.out.println(a == b); // true (String Pool 덕분에 같은 주소) 
System.out.println(a == c); // false (다른 객체)

✅ 2. .equals() (동등성 비교, 값 비교)

 

  • int, double 같은 원시타입에 대해선 ==가 값 비교다.
  • 참조타입(reference) 에 대해선 참조(주소) 비교.

 

  • 객체 안의 값이 같은지 비교
  • Object의 기본 equals()는 ==와 동일하지만,
    대부분의 클래스(String, Integer 등)는 값 비교로 오버라이드 되어 있음.
System.out.println(a.equals(b)); // true (문자열 값 같음) 
System.out.println(a.equals(c)); // true (문자열 값 같음)

📌 정리

비교 방법비교 대상결과
    == 참조(주소) 비교 같은 객체면 true, 값이 같아도 객체 다르면 false
.equals() 값 비교 (오버라이드된 경우) 객체 안의 내용이 같으면 true

✅ 추가 주의사항

  • 원시 타입(primitive) → ==로 값 비교 (int, double 등)
  • 래퍼 클래스(Integer, Long 등) → == 쓰면 박싱된 객체의 참조 비교가 되므로 .equals() 써야 안전
 
Integer x = 128; 
Integer y = 128; 
System.out.println(x == y); // false (128은 캐싱 범위 밖) 
System.out.println(x.equals(y)); // true (값 같음

✅ ==의 안전성 및 null

==은 NullPointerException을 발생시키지 않음.

String s = null;
System.out.println(s == null); // true

반면 s.equals(...)는 s가 null이면 NPE 발생.


🔑 기억 포인트

  • 동일성(==) → 같은 객체인가?
  • 동등성(.equals) → 같은 값인가?

이렇게 알아보다 보니 인스턴스?를 어떻게 비교하고 알 수 있는건지 생각이 나지 않아서 추가적으로 알아보았다.

1) 인스턴스(객체)란?

  • 클래스 = 설계도, 인스턴스 = 설계도로 찍어낸 실제 물건
    예: class Person { ... } → new Person("kim") 는 Person 클래스의 인스턴스(객체).
  • 인스턴스는 JVM 힙(heap) 영역에 할당되고, 필드(데이터)와 객체 헤더(메타데이터)를 가진다.
  • 각 인스턴스는 자기만의 정체성(identity) 을 갖는다 — 다른 인스턴스와 구별되는 “하나의 실체”.

2) 레퍼런스(참조)와 주소 개념

  • Java 변수(참조형)는 객체를 가리키는 참조(reference) 를 담는다. 이 참조는 C의 포인터처럼 “어디에 있는 객체”를 가리키는 역할을 한다는 개념적 동치야.
  • 하지만 JVM은 구현 세부(메모리 주소, GC 이동, compressed oops 등)를 추상화한다. 그래서:
    • "물리적 메모리 주소"를 직접 얻거나 신뢰하면 안 된다.
    • 대신 참조(핸들/포인터) 라는 추상적 개념을 통해 동일성(identity)을 비교한다.

3) 같은 인스턴스(동일 객체)를 어떻게 알 수 있나? (실제 방법)

  1. == 연산자(참조 비교)
    • 참조가 같은 객체 인스턴스를 가리키면 true. (즉, 두 변수가 동일한 인스턴스를 참조)
    • 예:
    •  
      Person a = new Person("A"); Person b = a; Person c = new Person("A"); System.out.println(a == b); // true (같은 인스턴스) System.out.println(a == c); // false (서로 다른 인스턴스)
  2. System.identityHashCode(obj)
    • 클래스가 hashCode()를 오버라이드했어도 객체의 identity 기반 해시값을 얻는다.
    • 동일 인스턴스이면 동일한 identityHashCode 값을 반환(대부분의 경우). 다만 완전한 메모리 주소는 아님.
  3. 디버거 / 힙 덤프
    • IDE 디버거(예: IntelliJ)나 VisualVM, jmap + Eclipse MAT로 힙 덤프를 뜨면 객체 ID/주소 수준으로 확인 가능.
  4. 특수한 경우(예외적)
    • enum 상수는 JVM이 하나의 인스턴스만 만든다 → == 사용 권장.
    • String 리터럴은 intern pool 때문에 같은 리터럴은 같은 인스턴스를 가리킬 수 있다 (==가 true).
    • 박싱된 숫자(Integer)는 -128~127 캐시 때문에 == 결과가 달라질 수 있음 → 주의.

4) 예제 코드(한 번에 이해되도록)

 
import java.util.Objects; class Person { String name; Person(String n) { this.name = n; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person p = (Person)o; return Objects.equals(name, p.name); } @Override public int hashCode() { return Objects.hash(name); } } public class IdentityDemo { public static void main(String[] args) { Person p1 = new Person("kim"); Person p2 = new Person("kim"); Person p3 = p1; System.out.println("p1 == p2 : " + (p1 == p2)); // false System.out.println("p1.equals(p2) : " + p1.equals(p2)); // true (값 동등) System.out.println("p1 == p3 : " + (p1 == p3)); // true (같은 인스턴스) System.out.println("idHash p1: " + System.identityHashCode(p1)); System.out.println("idHash p2: " + System.identityHashCode(p2)); System.out.println("idHash p3: " + System.identityHashCode(p3)); // p1과 동일 } }
import java.util.Objects; 

class Person { 
    String name; 
    Person(String n) { this.name = n; } 
    @Override 
    public boolean equals(Object o) { 
    	if (this == o) return true; 
        if (!(o instanceof Person)) return false; 
      	Person p = (Person)o; 
        return Objects.equals(name, p.name); 
    } 
    
    @Override 
    public int hashCode() { 
    	return Objects.hash(name); 
        } 
} 
    
public class IdentityDemo { 
    public static void main(String[] args) { 
        Person p1 = new Person("kim"); 
        Person p2 = new Person("kim"); 
        Person p3 = p1; 
            
        System.out.println("p1 == p2 : " + (p1 == p2)); // false 
        System.out.println("p1.equals(p2) : " + p1.equals(p2)); // true (값 동등) 
        System.out.println("p1 == p3 : " + (p1 == p3)); // true (같은 인스턴스) 
        System.out.println("idHash p1: " + System.identityHashCode(p1)); 
        System.out.println("idHash p2: " + System.identityHashCode(p2)); 
        System.out.println("idHash p3: " + System.identityHashCode(p3)); // p1과 동일 
    } 
}
  • 출력 요약: p1 == p2는 false(참조 다름), p1.equals(p2)는 true(같은 값). p1 == p3는 true.

5) 메모리 주소와 JVM의 현실적 한계

  • JVM 내부는 객체를 힙의 어느 위치에 저장하고, GC가 객체를 이동(compact)할 수 있다.
  • 따라서 물리적 메모리 주소는 런타임에 이동될 수 있고, 프로그래머가 직접 접근/신뢰하면 안 된다.
  • System.identityHashCode()는 아이덴티티를 나타내는 정수(대부분 JVM에서 객체 헤더 기반)지만 절대적인 물리 주소를 제공하지 않음.
  • 원하면 네이티브 계층(sun.misc.Unsafe, JNI)에서 주소를 얻을 수 있으나 안전하지 않고 비표준, 일반적이지 않음.

 

'개발 공부' 카테고리의 다른 글

Elasticsearch 써보자(2)  (0) 2025.04.23
Elasticsearch써보자(1)  (0) 2025.04.10
TPS(Transaction Per Second)  (0) 2025.03.28
WebSocket과 STOMP  (0) 2025.03.26
WebSocket 통신 방식이란?  (0) 2025.03.26