equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다. 그렇지 않으면 hashCode 일반 규약을 어기게 되어 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 생길 수도 있다.
아래는 Object 명세의 규약이다.
HashMap은 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않도록 최적화되어 있다. 따라서 hashCode를 제대로 정의하지 않았으면 아이템을 넣어도 꺼내려보면 없다고 할 수도 있다.
이상적인 해시함수는 서로 다른 인스턴스들을 32비트 정수 범위에 균일하게 분배해야 한다.
그러러면 아래의 요령을 따라야 한다.
Arrays.hashCode
를 사용한다.@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 사용자에게 자세히 알려주지 않아야 클라이언트가 이 값에 의존하지 않고, 추후에 계산 방식을 바꿀 수도 있다.