[이펙티브 자바] 아이템 11. equal를 재정의하려거든 hashCode도 재정의하라

June·2022년 3월 2일
0

[이펙티브자바]

목록 보기
11/72

hashCode 재정의 필요성

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다. 그렇지 않으면 hashCode 일반 규약을 어기게 되어 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 생길 수도 있다.

아래는 Object 명세의 규약이다.

  • equals 비교에 사용되는 정보가 똑같으면 hashCode는 몇 번을 호출해도 일관되게 같은 값을 반환해야 한다.
  • equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
  • equals(Object)가 두 객체를 다르다고 판단했다고, 다른 hashCode를 반환할 필요는 없지만, 다른 값을 반환해야 해시테이블의 성능이 좋아진다.

HashMap은 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않도록 최적화되어 있다. 따라서 hashCode를 제대로 정의하지 않았으면 아이템을 넣어도 꺼내려보면 없다고 할 수도 있다.

좋은 hashCode 재정의

이상적인 해시함수는 서로 다른 인스턴스들을 32비트 정수 범위에 균일하게 분배해야 한다.
그러러면 아래의 요령을 따라야 한다.

  • 객체의 나머지 핵심 필드 각각에 대해 다음 작업을 수행한다.
  • 해당 필드의 해시코드 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일까?
홀수이면서 소수이기 때문이다.
짝수 즉, 2를 곱하게 되면 시프트 연산과 같은 결과를 내기 때문이다.
시프트 연산과 같은 결과를 내게 된다는 것은 비트 연산에서 오른쪽이 0으로 채워진다. 즉 서로 다른 숫자였는데 똑같이 0으로 채워져서 겹칠 확률이 많아진다는 것 같다.

소수를 사용하는 이유는 소수를 사용했을 때 클러스터링이 덜 발생했다는 분포가 있었다는 것을 어디서 본거 같다.

성능 개선

클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기보다는 캐싱하는 방식을 고려해야 한다. 이 타입의 객체가 주로 해시의 키로 사용될 것 같다면 인스턴스가 만들어질때 해시코드를 계산해둬야 한다. 해시의 키로 사용되지 않다면 hashCode가 처음 불릴 때 계산하게 지연 초기화를 사용할 수도 있다. 필드를 지연 초기화하려면 스레드 안전하게 만들어야 한다(아이템 83).

성능을 높이겠다고 해시코드를 계산할 때 핵심 필드를 생략해서는 안된다. 그러면 해시테이블의 성능이 떨어질 수도 있다.,

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

0개의 댓글