2111 단어
11 분
[ JAVA ] 자바의 동일성(Identity)과 동등성(Equality)

들어가면서#

이번 포스트에서는 자바에서의 동일성(Identity)과 동등성(Equality)에 대해 알아보려 한다. 자바는 객체, 래퍼클래스라는 개념으로 인해 원시값과 객체, 그리고 래퍼클래스간의 비교가 헷갈릴 수 있다. 이번 기회에 헷갈리지 않게 정리해두려 한다.

동일성(Identity)#

동일성은 두 객체가 메모리 상에서 같은 객체인지 비교하는 개념이다. 자바에서는 == 연산자를 사용하여 객체의 동일성을 비교한다. == 연산자는 객체의 레퍼런스(참조)를 비교하므로, 두 변수가 동일한 객체를 가리키고 있는지를 확인한다.

public static void main(String[] args) {
    Apple apple1 = new Apple(100);
    Apple apple2 = new Apple(100);
    Apple apple3 = apple1;

    System.out.println(apple1 == apple2); // false
    System.out.println(apple1 == apple3); // true
}

apple1과 apple2는 참조가 다르기 때문에 == 연산 결과 false가 반환되지만, apple1의 참조를 가지는 apple3은 == 연산 결과 true를 반환한다.

동등성(Equality)#

동등성은 논리적으로 객체의 내용이 같은지를 비교하는 개념이다. 자바에서는 equals() 메서드를 사용하여 객체의 동등성을 비교한다. Apple 클래스를 예시로 보면, Object.equals 메서드를 오버라이딩하여 객체의 실제 데이터를 비교하도록 했다. 그래서 apple과 anotherApple은 다른 객체이지만, 무게가 같기 때문에 동등성 비교 결과 true가 반환된다.

public class Apple {

    private final int weight;

    public Apple(int weight) {
        this.weight = weight;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Apple apple = (Apple) o;
        return weight == apple.weight;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(weight);
    }

    public static void main(String[] args) {
        Apple apple = new Apple(100);
        Apple anotherApple = new Apple(100);

        System.out.println(apple.equals(anotherApple)); // true
    }
}

왜 equals() 메서드를 오버라이딩 한거지?#

public class Object {
    ...
    public boolean equals(Object obj) {
        return (this == obj);
    }
    ...
}

Object 클래스의 equals() 메서드는 == 연산자를 사용하여 동일성을 비교한다. 그리고 모든 클래스는 Object 클래스를 상속하여 동일성 비교를 기본으로 동작하기 때문에, 동등성 비교가 필요한 클래스에서 필요에 맞게 equals & hashCode 메서드를 오버라이딩해야 한다.

equals()와 ==의 차이#

equals()는 객체의 내용을 비교하는 반면, ==는 객체의 참조(레퍼런스)를 비교한다. 따라서 두 객체의 내용이 같더라도 서로 다른 객체라면 equals()는 true를 반환할 수 있지만,==는 false를 반환한다.

equals() 구현 시 고려사항#

equals() 메서드를 올바르게 구현하기 위해서는 다음 5가지 조건을 충족해야 한다.

  1. 반사성(Reflexivity): 모든 객체 x에 대해, x.equals(x)는 true여야 한다.
  2. 대칭성(Symmetry): 모든 객체 x, y에 대해, x.equals(y)가 true이면 y.equals(x)도 true여야 한다.
  3. 추이성(Transitivity): 모든 객체 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)가 true이면, x.equals(z)도 true여야 한다.
  4. 일관성(Consistency): 객체가 변경되지 않는 한, x.equals(y)의 결과는 항상 같아야 한다.
  5. null 비교: 모든 객체 x에 대해, x.equals(null)은 false여야 한다.

아래는 이러한 조건을 충족하는 equals() 메서드의 구현 예시이다.

public class Person {
    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public boolean equals(Object o) {
        // 반사성 검사 및 성능 최적화
        if (this == o) return true;
        
        // null 검사 및 타입 비교
        if (o == null || getClass() != o.getClass()) return false;
        
        // 타입 캐스팅
        Person person = (Person) o;
        
        // 필드 비교
        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }
    
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
    
    public static void main(String[] args) {
        Person p1 = new Person("홍길동", 30);
        Person p2 = new Person("홍길동", 30);
        Person p3 = new Person("홍길동", 25);
        
        System.out.println(p1.equals(p1)); // 반사성: true
        System.out.println(p1.equals(p2) && p2.equals(p1)); // 대칭성: true
        System.out.println(p1.equals(p3)); // 내용 다름: false
    }
}

이러한 equals() 구현은 객체의 논리적 동등성을 올바르게 판단하며, 위의 5가지 조건을 모두 만족한다. 또한 hashCode() 메서드도 함께 구현하여 해시 기반 컬렉션에서도 정상적으로 동작하도록 한다.

원시 타입(Primitive Types)과 참조 타입 비교의 차이점#

자바에서 원시 타입(int, boolean, char 등)과 참조 타입은 비교 방식에 차이가 있다. 원시 타입은 값 자체를 저장하기 때문에 == 연산자로 값을 직접 비교한다. 반면 참조 타입은 메모리 주소를 저장하므로 == 연산자는 두 변수가 같은 객체를 가리키는지(즉, 같은 메모리 주소를 가지는지) 비교한다.

public class TypeComparison {
    public static void main(String[] args) {
        // 원시 타입 비교
        int a = 5;
        int b = 5;
        System.out.println(a == b); // true (값 비교)
        
        // 참조 타입 비교
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = obj1;
        System.out.println(obj1 == obj2); // false (다른 객체)
        System.out.println(obj1 == obj3); // true (같은 객체)
    }
}

원시 타입은 equals() 메서드를 가지지 않기 때문에 항상 == 연산자를 사용해야 한다. 반면, 객체는 내용 비교를 위해 equals() 메서드를 사용해야 한다.

String은 객체인데 == 비교해도 되던데 뭐지?#

문자열 리터럴은 문자열 상수 풀(String Constant Pool) 에 저장되기 때문에, 동일한 문자열 리터럴을 참조하면 == 연산자가 true를 반환할 수 있다. 하지만 new 키워드를 사용하여 문자열을 생성하면 새로운 객체가 생성되므로 == 연산자가 false를 반환할 수 있다. 따라서 문자열 비교 시 항상 equals() 메서드를 사용한 동등성 비교를 하는 것이 좋다.

public class StringComparison {
    public static void main(String[] args) {
        String str1 = "안녕하세요";
        String str2 = "안녕하세요";
        String str3 = new String("안녕하세요");
        
        // 동일성 비교
        System.out.println(str1 == str2); // true
        System.out.println(str1 == str3); // false
        
        // 동등성 비교
        System.out.println(str1.equals(str2)); // true
        System.out.println(str1.equals(str3)); // true
    }
}

// String.class equals 오버라이딩 되어있음.
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    return (anObject instanceof String aString)
            && (!COMPACT_STRINGS || this.coder == aString.coder)
            && StringLatin1.equals(value, aString.value);
}

Integer 같은 래퍼 클래스는 어떻게 비교하지?#

래퍼 클래스도 객체이기 때문에 == 연산자는 참조를 비교한다. 값 비교를 원할 경우 equals() 메서드를 사용해야 한다. 다만, 자바는 특정 범위의 래퍼 객체를 캐싱하므로 같은 값의 Integer 객체가 같은 참조를 가질 수 있다(-128 ~ 127). 하지만 일반적으로 equals()를 사용하는 것이 안전하다.

래퍼 클래스의 오토박싱/언박싱 영향#

자바 5부터 도입된 오토박싱(Autoboxing)과 언박싱(Unboxing)은 원시 타입과 래퍼 타입 간의 자동 변환을 지원한다. 이 기능은 비교 연산에도 영향을 미치므로 주의가 필요하다.

public class WrapperComparison {
    public static void main(String[] args) {
        Integer num1 = 127;
        Integer num2 = 127;
        System.out.println(num1 == num2); // true (캐싱된 같은 객체)
        
        Integer num3 = 128;
        Integer num4 = 128;
        System.out.println(num3 == num4); // false (캐싱 범위 초과)
        
        // 오토박싱/언박싱 영향
        int primitive = 100;
        Integer wrapper = 100;
        System.out.println(primitive == wrapper); // true (언박싱 후 값 비교)
        
        // 안전한 비교
        System.out.println(num3.equals(num4)); // true (값 비교)
    }
}

위 예제에서 볼 수 있듯이, 래퍼 클래스와 원시 타입을 ==로 비교할 경우 래퍼 클래스가 언박싱되어 원시 값 비교가 일어난다. 하지만 래퍼 클래스끼리 비교할 때는 객체 비교가 일어나므로 의도치 않은 결과가 발생할 수 있다. 특히 Integer의 경우 -128부터 127까지의 값은 내부적으로 캐싱되어 같은 객체를 참조하지만, 그 범위를 벗어나면 다른 객체를 생성한다. 따라서 래퍼 클래스 간 비교는 항상 equals() 메서드를 사용하는 것이 안전하다.

출처#

[ JAVA ] 자바의 동일성(Identity)과 동등성(Equality)
https://blog-full-of-desire-v3.vercel.app/posts/java/java-identity-equality/
저자
SpeculatingWook
게시일
2025-02-28
라이선스
CC BY-NC-SA 4.0