본문 바로가기
Language & Tool/Java

☕자바에서 "같다"라는 두 가지 의미

by 박수무당벌레 2026. 7. 13.

1. 동일성 vs 동등성

1) 동일성 (Identity)

  • 개념: 두 객체/변수가 메모리상에서 완전히 같은 인스턴스(동일한 주소/참조값)를 가리키고 있는지 비교. 즉, 물리적으로 같은 객체인지를 판단한다.
  • 비교 방법: == 사용 (참조값 비교)
  • 특징:
    • 참조값이 같으면 두 객체는 실제 동일한 객체이므로 내부의 값(Value)도 당연히 같다.
    • equals() 메서드를 구현할 때 성능 최적화를 위해 내부 최우선 조건문으로 if (this == anObject) 형태를 자주 사용한다.
Person p1 = new Person("홍길동");
Person p2 = p1; // p2는 p1과 동일한 메모리 주소를 참조함

System.out.println(p1 == p2); // true (동일함)

2) 동등성 (Equality)

  • 개념: 두 객체의 참조값이 다르더라도(서로 다른 메모리에 있더라도), 내용(값)이 논리적으로 같은지 비교한다.
  • 비교 방법: equals() 메서드 오버라이딩
    • Object 클래스의 기본 equals()는 ==와 동일 (오버라이딩 필요)
  • 특징:
    • 클래스를 개발자가 정의한 기준에 따라 두 객체의 값이 같은지 판단한다.
    • 대표적으로 String 같은 모든 래퍼 클래스들은 equals()를 오버라이딩하여 래퍼로 감싼 진짜 값(value)을 비교하도록 구현되어 있다.(문자열 구성 내용이 동일한지 등)
String str1 = new String("Hello");
String str2 = new String("Hello");

System.out.println(str1 == str2);      // false (메모리 주소가 다르므로 동일하지 않음)
System.out.println(str1.equals(str2)); // true  (내부 문자열 값이 같으므로 동등함)
더보기

💡 Object 클래스 추가 설명

 

1) Object 상속 (String 클래스 선언부)

  • 자바의 모든 클래스는 direct 또는 indirect(간접) 방식으로 최상위 클래스인 Object를 자동으로 상속받는다.
    • 상속 체인: 명시적으로 다른 클래스를 상속받더라도(자바는 단일 상속만 지원), 결국 최상위까지 올라가면 상속받게 됨
    • 명시적으로 다른 클래스를 상속받지 않으면, 컴파일러가 바이트코드를 만들 때 extends Object를 직접 주입해 준다.
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // extends Object 가 생략되어 있음
}

 

2) Object의 equals() 구현 내용

  • equals()는 자바의 모든 객체가 물려받는 메소드
public boolean equals(Object obj) {
    return (this == obj); // 주소값(==)만 비교
}
💡 기본 자료형은 클래스/객체가 아니기 때문에 메소드 자체를 가지지 못 한다.

2. String, Integer로 알아보는 동일성과 동등성

1) String로 설명하기

  • str1 == str2 (true):
    • 리터럴("")로 생성한 문자열은 Heap 영역 내의 문자열 리터럴 재사용 풀(String Constant Pool)에 저장된다.
    • 자바는 메모리 절약을 위해 기존에 존재하는 "Hello"의 주소를 재사용하므로, str1과 str2는 동일한 주소를 가리킴.
  • str1 == str3 (false):
    • new String()을 사용하면 리터럴 풀과 별개로 새로운 메모리 공간(Heap)에 객체를 생성한다.
    • 따라서 주소값이 달라 동일하지 않는다.
  • str1.equals(str3) (true): 
    • String 클래스는 equals()를 재정의하여 문자열의 내용만 비교하도록 구현되어 있음.
    • 주소는 달라도 들어있는 문자열이 "Hello"로 같으므로 동등하다.
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");

// 1. 동일성 비교 (==)
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false

// 2. 동등성 비교 (equals)
System.out.println(str1.equals(str3)); // true
더보기

ℹ️ 실제 equals 내부 구현 코드 (java.lang.String.java)

 

1) String.equals() 동작 원리 (단순 일치 여부)

  • 타입: byte[] (바이트 배열)
  • 특징:
    • Compact Strings 기능이 도입되면서 변경됨. (Java 9 ~)
    • 기존 2바이트 공간을 쓰던 문자열 중 영문/숫자처럼 1바이트만으로 충분한 문자열은 Latin-1(1바이트)로 다루고, 한글/한자 같은 문자는 UTF-16(2바이트)으로 저장한다.
    • 이를 구분하기 위해 인코딩 플래그 변수인 coder(LATIN1 또는 UTF16) 필드가 함께 추가됨.
    • @Stable 어노테이션은 JVM(JIT 컴파일러) 수준에서 배열 요소가 변경되지 않음을 보장받아 정적 최적화를 수행하는 역할.
  • 동작 원리:
    1. 전달받은 Object가 Integer 타입인지 확인(instanceof). — 패턴 매칭
    2. Integer가 맞다면 다운캐스팅하여 객체가 감싸고 있는 기본형 필드 value(int)의 값을 == 연산자로 직접 비교.
    3. 다른 타입이거나 null이면 바로 false를 반환한다.
/** The value is used for character storage. */
@Stable
private final byte[] value;

/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
    
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                              : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}
ℹ️ Java 9 이전 버전은 모든 문자를 기본적으로 UTF-16 단위인 2바이트(char)로 저장. (낭비)
ℹ️ if(super.equals(anObject)) 도 가능하지만 메소드 호출 과정이 한 번 더 추가되어 성능상 손해(오버헤드)

 

2) (참고) 비슷하게 값을 비교하는 String.compareTo() 동작 원리 (정수값 반환)

  1. 문자 단위 순차 비교 (for 루프): 두 문자열의 첫 번째 글자부터 순서대로 한 글자씩 비교.
  2. 달라지는 시점의 문자로 판단 (return char1 - char2):
    • 비교 도중 처음으로 달라지는 문자가 등장하면, 두 문자의 코드값(ASCII / Unicode) 차이인 this_char - other_char 결과를 즉시 리턴한다.
    • 예: "apple" vs "banana" ➔ 'a' - 'b' 연산 결과인 음수(-1) 반환
  3. 길이 차이로 판단 (return len1 - len2):
    • 한쪽 문자열이 다른 쪽의 접두어(Prefix)인 경우(예: "app" vs "apple"), 짧은 길이(lim) 동안은 문자가 전부 같다.
    • 이때는 더 이상 비교할 문자가 없으므로 두 문자열의 길이 차이(len1 - len2)를 반환다.
    • 예: "app"(길이 3) vs "apple"(길이 5) ➔ 3 - 5 연산 결과인 음수(-2) 반환
public int compareTo(String anotherString) {
    byte v1[] = value;
    byte v2[] = anotherString.value;
    
    // 두 문자열의 인코딩(Latin1 vs UTF16)이 같은 경우
    if (coder() == anotherString.coder()) {
        return isLatin1() ? StringLatin1.compareTo(v1, v2)
                          : StringUTF16.compareTo(v1, v2);
    }
    
    // 두 문자열의 인코딩이 서로 다른 경우 (Latin1 vs UTF16)
    return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
                      : StringUTF16.compareToLatin1(v1, v2);
}

2) Integer로 설명하기

  • 왜 a == b (100 비교) 와 c == d (200 비교) 결과값이 다를까?:
    • Integer a = 100;을 실행하면 캐시되어 있는 기존 100 객체의 주소를 공유하므로 a == b는 true가 된다.
    • 반면 200은 캐싱 범위를 벗어나므로 매번 새로운 Integer 객체를 생성하여 주소가 달라지므로 c == d는 false가 된다.
  • Integer Caching (캐싱 메커니즘):
    • 자바의 Integer 클래스는 자주 사용하는 -128 ~ 127 범위의 정수 값을 내부적으로 미리 캐싱(Integer Cache)해 둠.
    • ⚠️ 때문에 new 키워드 대신 일반적인 오토박싱(Auto-boxing) 형태로 생성할 때는 주의가 필요하다.
Integer num1 = new Integer(100); // (참고: Java 9 이후 deprecated)
Integer num2 = new Integer(100);

// 1. new 키워드로 생성 시
System.out.println(num1 == num2);      // false (주소 다름)
System.out.println(num1.equals(num2)); // true  (값 동일)

// 2. 오토박싱 형태로 생성 시
Integer a = 100;
Integer b = 100;

Integer c = 200;
Integer d = 200;

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

🚨 결론: Integer 같은 래퍼의 값을 비교할 때는 주소값이 달라지는 상황을 예방하기 위해 반드시 == 대신 equals()를 사용해야 한다.

더보기

ℹ️ 실제 equals() 내부 구현 코드

 

1) Integer.equals() 동작 원리 (단순 일치 여부)

  1. 전달받은 Object가 Integer 타입인지 확인(instanceof). — 패턴 매칭
  2. Integer가 맞다면 다운캐스팅하여 객체가 감싸고 있는 기본형 필드 value(int)의 값을 == 연산자로 직접 비교.
  3. 다른 타입이거나 null이면 바로 false를 반환한다.
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

 

2) (참고) Integer.compareTo() 동작 원리 (값 비교 후 정수값 반환)

  1. this.value와 상대방의 anotherInteger.value를 가져온다.
  2. 삼항 연산자를 통해 두 int 값의 크기를 판단한다:
    • x < y 이면 -1
    • x == y 이면 0
    • x > y 이면 1
  3. 양수/0/음수라는 정수(int) 값을 리턴하여 어느 쪽이 더 큰지 위치/순서를 알려줌 
public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

// Integer.compare() 정적 메서드
public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}