equals 와 hashCode 를 함께 정의해야 하는 이유

yoondgu·2023년 2월 25일
0

Java 

목록 보기
16/18
post-thumbnail

(해시 어쩌구 볼 때마다 해시브라운이 생각난다..)
우테코 백엔드 5기 과정 중 학습하며 작성한 내용입니다.

Object 클래스와 재정의(Overriding)

Java의 모든 클래스는 Object 클래스를 상속합니다.
우리가 클래스를 만들 때, extends Obejct 를 써주지 않아도 컴파일러가 이를 알아서 처리해줍니다.
만약 다른 클래스를 상속하더라도 결국 조상으로는 Object 클래스가 있습니다.
즉 Object 클래스는 Java의 모든 클래스에 대한 최고 조상 클래스인 셈이고, 모든 클래스는 Object의 메서드를 사용할 수 있습니다.

Object 클래스에는 총 11개만의 메서드가 있는데, equals와 hashCode는 그 중에서도 재정의(overriding) 할 수 있는 메서드* 중 하나입니다.

  • 두 메서드 외에는 toString, clone, finalize가 있으며 그 외에는 final 메서드로 재정의를 금지하고 있습니다.

논리적으로 같은 객체는 같은 해시코드를 반환하도록 하자

그렇다면 내가 만든 클래스에서 이 메서드들을 아무렇게나 원하는 대로 재정의하면 될까요?
관련 규약에 대한 전제를 공유하는 클래스들을 오동작시키지 않으려면, 일반 규약에 맞게 재정의해주어야 합니다.

Object 명세 중 아래 내용이 이 질문(equals 와 hashcode 를 함께 정의해야 하는 이유는?)에 대한 힌트가 될 수 있겠습니다.

equals(Obejct)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.

Hash값을 사용하는 Collection 클래스인 HashMap, HashSet, HashTable은 위 규약을 전제로 하고 있기 때문입니다.

이 클래스들은
1. Hash값이 서로 같고
2. equals의 반환값이 true
일 때 두 객체가 논리적으로 같다고 판단합니다.

예를 들기 위해 equals만 재정의한 클래스 Id를 만들었습니다.
HashMap의 key값으로 이 클래스의 인스턴스를 사용한다면 어떨까요.

public class Id {

    private final int value;

    public Id(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Id id = (Id) o;
        return value == id.value;
    }
}

아래 테스트를 돌려보면 모두 통과함을 확인할 수 있습니다.

  • 두 아이디는 서로 동등하다고 판단되지만,
    해시값이 다르기 때문에
  • 우리가 HashMap을 사용할 때 기대하는 결과와 달리 번호가 같은 아이디가 서로 다른 엔트리로 저장됩니다.
  • 번호가 같은 아이디 객체로 원하는 엔트리를 찾아 조회할 수 없습니다.
class IdTest {

    @Test
    void 번호값이_같은_두_아이디는_동등하다() {
        Id firstId = new Id(1);
        Id secondId = new Id(1);

        assertThat(firstId.equals(secondId)).isTrue();
    }

    @Test
    void 해시값이_다르면_다른_객체이다() {
        Map<Id, String> names = new HashMap<>();

        names.put(new Id(1), "doy");
        names.put(new Id(1), "doy");

        assertThat(names).hasSize(2);
    }

    @Test
    void 해시값이_다른_객체로_조회할_수_없다() {
        Map<Id, String> names = new HashMap<>();

        names.put(new Id(1), "doy");

        assertThat(names.get(new Id(1))).isNull();
    }
}

예측 가능한 방식으로 사용하기

결론을 정리하자면

Hash값을 사용하는 Collection 클래스를 오동작시키지 않으려면 equals와 hashCode 메서드를 함께 정의해야 합니다.

무엇이든 절대적으로, 항상, 무조건 이라고 말할 수는 없겠지만
Object라는 최고 조상 클래스의 일반 규약에 따라 하위 클래스들이 구현되어있기 때문에
이를 예측 가능한 방식으로 사용하기 위해서는 주어진 규약에 맞게 재정의해야 한다는 생각이 듭니다.
(굳이 재정의를 해야 한다면 말입니다.)
그에 대한 좋은 예시가 equals와 hashCode를 함께 정의해야 하는 이유인 것 같습니다.


참고자료
이펙티브 자바 Effective Java 3/E - 아이템 10. equals는 일반 규약을 지켜 재정의하라
이펙티브 자바 Effective Java 3/E - 아이템 11. equals를 재정의하려거든 hashCode도 재정의하라
테코블 2기_둔덩님 포스트 equals와 hashCode는 왜 같이 재정의해야 할까?

6개의 댓글

comment-user-thumbnail
2023년 2월 25일

습관적으로 equals와 hashCode를 재정의해서 사용했었는데 ㅎㅎ
테스트코드와 함께 보니 더 좋은 것 같습니다 !!
잘 보고 갑니당

1개의 답글
comment-user-thumbnail
2023년 2월 26일

해시브라운 사진을 보니 갓튀긴 해시브라운 한입 먹고싶네요..🤤
테스트코드와 함께 가독성 좋은 글 잘 보고 갑니다!

1개의 답글
comment-user-thumbnail
2023년 2월 26일

해시브라운 맛있겠당..🥹
도이 정리를 정말 잘하시네요🔥🔥 잘보고갑니당

1개의 답글