[Java] Why, How - equals를 오버라이딩할 때 hashCode도 같이 오버라이딩 해야 되는 이유.

하쮸·2024년 12월 26일

Error, Why, What, How

목록 보기
10/68

equals오버라이딩할 때 hashCode도 같이 오버라이딩 해야 되는 이유에 대해 정리한 글.

  • 1번이 이해가 잘 되지 않는다면 2, 3, 4내용을 한번 훑어보고 보는 걸 추천함.

1. equals()와 hashCode()

  • 동일한 두 객체는 동일메모리 주소를 가짐.

    • 그래서 동일한 객체동일한 해시코드를 가져야함.
    • 따라서 equals() 메서드를 오버라이딩해야 한다면 hashCode() 메서드도 같이 오버라이딩 해야함.
  • 두 객체가 equals()메서드의 결과값이 동일하다면, 두 객체의 hashCode() 값도 동일해야하고,
    두 객체가 equals()메서드의 결과값이 동일하지 않다면, 두 객체의 hashCode() 값은 동일하지 않아도 됨.

    • 즉, obj1.equals(obj2) == True --> hashCode(obj1) == hashCode(obj2)이여야함.
    • 하지만 hashCode(obj1) == hashCode(obj2) 라고 해서 obj1.equals(obj2) == True일 필요는 없음.
  • String 클래스는 Object로부터 상속 받은 hashCode()오버라이딩해서 문자열 내용으로 해쉬코드를 만듦.

    • 그래서 서로 다른 String 인스턴스라도 같은 내용의 문자열을 가지고있다면 hashCode()를 호출하면 같은 해쉬코드를 얻음.
  • 서로 다른 두 객체에 대해 equals()의 결과값이 true이면서 hashCode()의 반환값이 같아야 같은 객체로 인식함.

    • 그래서 새로운 클래스를 정의할 때 equals()오버라이딩을 통해 재정의 한다면 hashCode()도 같이 재정의해서 equals()의 결과가 true인 두 객체의 해쉬코드(hashCode())의 결과값이 항상 같도록 해줘야 됨.
    • 그렇지 않으면 해싱을 구현한 컬렉션 클래스에서는 equals()의 호출결과가 true이지만 해쉬코드다른 두 객체를 서로 다른 것으로 인식하고 따로 저장함.

equals()만 재정의(오버라이딩)

public class tmp {
    public static void main(String[] args) {
        HashMap<PersonTest, String> map = new HashMap<>();

        PersonTest p1 = new PersonTest("Hajju");
        PersonTest p2 = new PersonTest("Hajju");
        System.out.println("p1.equals(p2) : " + p1.equals(p2));

        map.put(p1, "Java");
        map.put(p2, "Spring");      // 동일한 name이지만 다른 객체로 인식

        System.out.println("map.size() : " + map.size()); // 결과: 2 (잘못된 결과)
    }
}

class PersonTest {
    String name;

    PersonTest(String name) {
        this.name = name;
    }

    // equals()만 재정의
    @Override
    public boolean equals(Object o) {
        if (o instanceof PersonTest) {
            PersonTest p = (PersonTest) o;
            return this.name.equals(p.name);
        }
        return false;
    }
//    @Override
//    public int hashCode() {
//        return name.hashCode(); // name이 같으면 같은 해시값 반환
//    }
}
-- 실행 결과 --
p1.equals(p2) : true
map.size() : 2

equals(), hashCode() 둘 다 재정의(오버라이딩)

public class tmp {
    public static void main(String[] args) {
        HashMap<PersonTest, String> map = new HashMap<>();

        PersonTest p1 = new PersonTest("Hajju");
        PersonTest p2 = new PersonTest("Hajju");
        System.out.println("p1.equals(p2) : " + p1.equals(p2));

        map.put(p1, "Java");
        map.put(p2, "Spring");      // 동일한 name이지만 다른 객체로 인식

        System.out.println("map.size() : " + map.size()); // 결과: 2 (잘못된 결과)
    }
}

class PersonTest {
    String name;

    PersonTest(String name) {
        this.name = name;
    }

    // equals() 재정의
    @Override
    public boolean equals(Object o) {
        if (o instanceof PersonTest) {
            PersonTest p = (PersonTest) o;
            return this.name.equals(p.name);
        }
        return false;
    }
    @Override
    public int hashCode() {
        return name.hashCode();     // name이 같으면 같은 해시값 반환
    }
}
-- 실행 결과 --
p1.equals(p2) : true
map.size() : 1


2. 동등성 (equals)

  • 동등하다는 의미.
  • 즉, 두 객체가 같은 값을 갖고 있는 경우를 의미함.
    • 동등성은 변수가 참조하고 있는 객체의 주소가 서로 달라도 안에 저장되어 있는 값이 같으면 두 변수는 동등함.
    • 동일하면 -> 동등함.
    • 동등하다고 해서 동일한 것은 아님.
  • equals() 메서드는 동등성(equality)을 비교함.
    • 즉, 두 객체의 내용이 같은지를 확인.
    • 여기서 말하는 equals메서드는 Object에 정의되어 있는 것이 아닌, 오버라이딩 된 것임.
      • Object 클래스에 정의된 기본 equals() 메서드== 연산자와 동일하게 주소값을 비교함.
    • 대표적인 예시로 String, File, Date, wrapper(Integer, Double 등)
      • 위에 언급한 클래스들은 내용비교하도록 오버라이딩 되어 있음.
String str1 = new String("hello");
String str2 = new String("hello");

System.out.println(str1.equals(str2));
System.out.println(str1 == str2);
-- 실행 결과 -- 
true  (내용 비교)
false (다른 메모리 주소)
  • equals()는 문자열의 값을 비교하여 true를 반환.
  • ==는 두 객체가 동일한 메모리 위치에 있는지 비교함. 다른 객체를 참조하므로 false를 반환.

2-1. Object equals()

  • equals()는 클래스에 따라 다르게 구현할 수 있음.
  • 새로운 클래스를 정의할 때 equals()를 재정의, 즉 오버라이딩 하지 않으면 Object 클래스의 equals()를 사용함.
  • Object에 정의되어 있는 equals()는 객체 비교, 즉 객체의 주소(참조 변수값)를 비교함.
    • 동일성 비교.
    • equals 메서드는 오버라이딩하지 않을 경우 내부적으로 == 연산자와 같은 로직을 수행함.
public boolean equals(Object obj) {
    return (this == obj); // 기본 구현
}

2-2. String equals()

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

JDK 21

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);
}

3. 동일성 (==)

  • 두 객체가 같은 메모리 주소를 참조하는 경우를 의미.
    • == 연산자를 사용.
  • 두 객체가 물리적으로 동일한 객체인지 확인.
Number number1 = new Number();
Number number2 = number1;

System.out.println(number1 == number2);
-- 실행 결과 --
true
  • 따라서 ==연산자는 동일성(identity)을 비교함.

    • 두 변수가 메모리에서 같은 객체를 가리키고 있는지를 확인.
  • 기본 타입(Primitive Types)

    • int, char, double 등에서는 변수가 가진 실제 값을 비교.
  • 참조 타입(Reference Types)

    • String, Object 등에서는 변수가 가리키는 객체의 메모리 주소를 비교함.
    • 두 변수가 서로 다른 객체를 참조하고 있다면, 내용이 같더라도 false를 반환.

4. 동일성 vs 동등성.

  • 두 객체에게 할당된 메모리 주소가 같으면 동일.
    • 동일성은 == 연산자.
      • == 연산자는 객체의 동일성을 판별하기 위해 사용.
  • 두 객체의 이 같으면 동등.
    • 동등성은 equals().
      • equals() 메서드는 두 객체의 동등성을 판별하기 위해 사용.
  • ==연산자
    • 주소값을 비교 (동일성)
  • equals()
    • 내용을 비교 (동등성, 재정의 필요)

5. 참고.

profile
Every cloud has a silver lining.

0개의 댓글