아이템11 equals를 재정의 하려거든 hashCode도 재정의 하라

Effective Java 3e 아이템 11을 요약한 내용 입니다.

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다. 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.

다음은 Object 명세에서 발췌한 규약이다.

  • equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.

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

  • equals가 두 객체를 다르다고 판단 했더라도 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.

hashCode 재정의를 잘못했을 때 크게 문제가 되는 조항은 두 번째다 즉 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.

HashMap은 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않도록 최적화되어 있으므로 별도의 객체를 HashMap의 Key로 사용할 경우 원하는 값을 리턴 받지 못할 것이다.

동등성이랑 동일성은 무엇일까? 동등성(동치성) : 두 객체가 동일한 정보를 담고 있는지? 동일성 : 두 객체가 물리적(주소)으로 같은지?

    @Override public int hashCode() { return 42; }

이 코드는 동치인 모든 객체에서 똑같은 해시코드를 반환하니 적법하다. 하지만 끔찍하게도 모든 객체에게 똑같은 값만 내어주므로 모든 객체가 해시테이블의 버킷 하나에 담겨 마치 연결 리스트처럼 동작한다.

좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환한다.

다음은 hashCode를 작성하는 간단한 요령이다.

  • 객체의 나머지 핵심 필드 각각에 대해 다음 작업을 수행한다.

  • 해당 필드의 해시코드 c를 계산한다.

    • 기본 타입 필드라면 Type.hashCode(f)를 수행한다.

    • 참조 타입 필드일 경우 표준형을 만들어 hashCode를 호출한다. 필드의 값이 null이면 0을 사용한다.

    • 필드가 배열이라면 핵심 원소 각각을 별도 필드처럼 다룬다. 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.

  • 이전 단계에서 계산한 해시코드 c로 result를 갱신한다.

    • result = 31 * result + c

  • result를 반환한다.

      @Override public int hashCode() {
          int result = Short.hashCode(areaCode);
          result = 31 * Short.hashCode(prefix);
          result = 31 * Short.hashCode(linenum);
          return result;
      }

그렇다면 왜 31을 사용했을까?

31이 홀수 이면서 소수(prime)이기 때문이다. 만약 이 숫자가 짝수이고 오버플로우 가 발생한다면 정보를 잃게 된다.

위와 같은 계산식이 번거로울 수 있다. 이를 간편하게 제공하기 위한 방법으로 Objects 클래스의 정적 메서드를 제공한다.

Objects 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 정적 메서드인 hash를 제공한다.

    @Overrider public int hashCode() {
        return Objects.hash(linenum, prefex, areaCode);
    }

이 메서드를 활용하면 앞서의 요령대로 구현한 코드와 비슷한 수준의 hashCode 함수를 단 한줄로 작성할 수 있다. 하지만 아쉽게도 속도는 더 느리다. 입력 인수를 담기 위한 배열이 만들어지고, 입력 중 기본 타입이 있다면 박싱언박싱도 거쳐야 하기 때문이다. 그러니 hash 메서드는 성능에 민감하지 않은 상황에서만 사용하자

클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기 보다는 캐싱하는 방식을 고려해야 한다.

hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자. 그래야 클라이언트가 이 값에 의지하지 않게 되고, 추후에 계산 방식을 바꿀 수도 있다.

정리

equals를 재정의할 때는 hashCode도 반드시 재정의해야 한다. 그렇지 않으면 프로그램이 제대로 동작하지 않을 것이다. 재정의한 hashCode는 Object의 API 문서에 기술된 일반 규약을 따라야 하며, `서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 구현해야 한다.렇게 구현하기가 어렵지는 않지만 조금 따분한 일이긴 하다. 하지만 걱정마시라toValue``레임워크를 사용하면 멋진 equals와 hashCode를 자동으로 만들어준다.

Last updated