[Effective Java] 아이템 11 - equals를 재정의하여거든 hashCode도 재정의하라
equals를 재정의한 클래스 모두에서 hashCode도 재정의 해야한다. 그렇지 않으면 hashCode 규약을 어기게 되어 인스턴스가 HashMap이나 HashSet 같은 컬렉션의 원소로 사용 될 떄 어떤 문제를 일으킬지 모른다.
hashCode 규약
- equals 비교에 사용되는 정보가 변하지 않았다면 런타임 안에서 일관성을 유지해야 한다.
- equals가 두 객체를 같다고 판단 했다면, 두 hashCode값은 똑같아야 한다.
- equals가 두객체를 다르다고 판단 했다고 다른 hashCode를 가질 필요는 없지만 해시 테이블 성능이 떨어진다
해시코드와 해시 맵의 관계
해시 맵은 기본 적으로 key : value 쌍으로 이루어 져 있다. 특정 value 값을 얻기 위해서 'key'값을 알고 있어야 한다.
해시 맵에 데이터를 저장 할 때 'key'의 해시코드가 value가 저장 되는 배열의 인덱스가 된다. 그 후 value의 값을 얻어오기 위해서는 'key'를 다시 해시화 하여 해당 인덱스에 있는 값을 불러온다.
가장 큰 문제가 되는 조항이 두번 째이다. 즉 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
m.put(new PhNum(707,867,5309), "제니") 다음에 m.get(new PhNum(707,867,5309))을 실행하면 "제니"가 아닌 null을 반환하게 되는 데 이는 HashMap은 해시코드가 다른 것 끼리는 동치성 검사를 하지 않도록 최적화 되어있기 때문이다. 때문에 hashCode를 Overide 하여 동치라면 같은 해시 코드를 반환하도록 해야한다. 또한 다른 인스턴스라면 다른 해시코드를 반환해야 좋은 해시 함수이다.
해시 코드 만들기
- int 변수 result를 c로 초기화 한다. c는 해당 객체의 첫 핵심 필드(equals 비교에 사용한)를 2.a 방식으로 계산한 것
- 해당 객체의 나머지 핵심필드 f에 대해 다음 작업 수행
a. 해당 필드의 해시코드 c 계산
- ) 기본 타입 필드라면 Type.hashCode(f) 수행. Type은 기본 타입의 박싱 클래스
- ) 참조 타입이면서 이 클래스의 equals가 이 필드의 equals를 호출해 비교한다면 이 필드의 hashCode를 호출.
계산이 더 복잡해 질 것 같으면 표준형의 hashCode 호출. 필드의 값이 null이면 0사용 - ) 필드가 배열이라면 핵심 원소 각각을 필드로 취급. 배열에 핵심 원소가 하나도 없으면 상수사용(0추천)
모든 원소가 핵심원소라면 Array.hashCode사용
- result 반환
2.b 의 31을 곱하는 과정은 필드의 해시를 처리하는 순서에 따라 result 값이 달라지게 한다. 구성하는 철자가 같은 아나그램에서 해시코드가 중복되는 것을 막을 수 있다.
곱할 숫자가 31인 이유는 홀수 이면서 소수 이기 때문이다. 짝수이면은 오버플로우가 발생했을 때 데이터가 손실되기 때문이다.
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
이 메서드는 해당 클래스의 핵심 필드 3개만을 사용해 간단히 해시 코드를 구한다. 과정에 비결정적인 요소가 없어 동치 인스턴스는 같은 해시코드를 가진다.
Object 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 hash 메서드를 정적으로 지원하지만 속도는 위 코드보다 느리다.
클래스가 불변이고 해시코드를 계산하는 비용이 크다면 캐싱을 고려해 볼만 하다.
이 타입의 객체가 해시의 키로 사용될 경우 인스턴스 생성 시 해시코드를 계산해 줘야 하지만 그 반대의 경우 hashCode가 불릴 때 계산하는 지연 초기화 전략도 괜찮다. 한가지 주의 사항은 필드를 지연 초기화 하려면 thread-safe를 고려해야 한다.
지연 초기화란
객체가 생성될 때가 아닌 객체가 사용 될 때 초기화 하는 방식
private int hashCode;
@Override public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode =result;
}
return result;
}
주의사항
해시코드를 계산할 때 핵심 필드를 생략해서는 안된다. 해시 테이블의 성능이 심각하게 떨어질 수 있다.
어떤 필드는 인스턴스들의 해시코드를 넓게 퍼뜨려주는 효과가 있을 수도 있다.
hashCode 생성 규칙을 API 사용자에게 자세히 공표하지 않아야 클라이언트가 이 값에 의존하지 않고 추후 계산 방식을 바꿀 수 있다.