Item 11 - equals를 재정의하려거든 hashCode도 재정의하라
equals를 재정의한 클래스 모두에서 hashCode도 재정의해야한다.
그렇지 않으면 HashMap이나 HashSet 같은 컬렉션 원소로 사용할 때 문제가 발생한다.
Object hashCode 명세 규약
- equals 비교에 사용하는 정보가 변경되지 않았다면 hashCode는 매번 같은 값을 리턴해야 한다. (변경되거나, 애플리케이션을 다시 실행했다면 달라질 수 있다.)
- 두 객체에 대한 equals가 같다면, hashCode의 값도 같아야 한다.
- 두 객체에 대한 equals가 다르더라도, hashCode의 값은 같을 수 있지만 해시 테이블 성능을 고려해 다른 값을 리턴하는 것이 좋다.
1
2
3
Map<Student, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "제니");
System.out.println(m.get(new PhoneNumber(707, 867, 5309))); // null 반환
hashCode를 재정의하지 않아서 논리적으로 같은 두 객체가 서로 다른 해시코드를 반환하여 null을 반환한다.
이 문제는 hashCode 메서드를 재정의해주면 해결된다.
좋은 hashCode 작성법 👍
1
2
// 사용금지
@Override public int hashCode { return 42; }
위 hashCode 메서드는 모든 객체에게 똑같은 값만 내어준다.
이 경우 해시테이블이 마치 연결리스트처럼 동작하고, 평균 수행 시간이 O(1) → O(n)으로 느려진다.
이상적인 해시 함수는 다른 객체들을 32비트 정수 범위에 균일하게 분배해야 한다.
1
2
3
4
5
6
@Override public int hashCode() {
int result = Short.hashCode(areaCode); // 1
result = 31 * result + Short.hashCode(prefix); // 2
result = 31 * result + Short.hashCode(lineNum); // 3
return result;
}
- 핵심 필드 f 하나의 값의 해시값을 계산해서 result 값을 초기화한다.
- 나머지 핵심 필드 f 각각에 대해 다음 작업 수행한다.
- 해당 필드의 해시코드 c 계산
- 기본 타입은 Type.hashCode(f)
- 참조 타입은 해당 필드의 hashCode
- 배열은 모든 원소를 재귀적으로 위의 로직을 적용하거나, Arrays.hashCode 사용
- result = 31 * result + 해당 필드의 hashCode 계산값(c)
31을 곱하는 이유?
- 소수 특성 : 31은 소수이므로 서로 다른 필드 값들이 같은 해시 코드를 가질 확률을 낮춘다.
- 곱셈 최적화: 31은 2의 제곱수에서 1을 뺀 수다. 컴파일러 최적화를 통해 곱셈이 빠르게 수행될 수 있다.
31 * x
는(x << 5) - x
로 변환가능하다. (비트 시프트 연산)
- 해당 필드의 해시코드 c 계산
- result를 리턴한다.
hashCode 구현대안 😊
- 구글 구아바의
com.google.common.hash.Hashing
- Objects 클래스의
hash
메서드 - 캐싱을 사용해 불변 클래스의 해시 코드 계산 비용을 줄일 수 있다.
주의 사항 🚑
- 지연 초기화 기법을 사용할 때 스레드 안전성을 신경써야 한다.
- 성능 때문에 핵심 필드를 해시코드 계산할 때 빼면 안된다.
- 해시코드 계산 규칙을 API에 노출하지 않는 것이 좋다.
핵심 정리 💡
- equals를 재정의할 때는 hashCode도 반드시 재정의하자.
- 재정의한 hashCode는 Object의 API 문서의 일반 규약을 따라야하며, 서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 구현해야한다.
참고 🕶️
This post is licensed under CC BY 4.0 by the author.