[Why]
- equals를 재정의한 클래스에서 hashCode를 재정의하지 않으면 hashCode의 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap, HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.
[When]
- Object 명세에서 발췌한 규약
- equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. 단, 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.
- equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
- equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
[How]
(given)
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max) {
throw new IllegalArgumentException(arg + ": " + val);
}
return (short) val;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof PhoneNumber)) {
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum == lineNum
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
... // 나머지 코드는 생략
}
(then)
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
[포함 필드]
- 성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다. 해시 품질이 나빠져 해시테이블의 성능을 심각하게 떨어뜨릴 수도 있다. 파생 필드는 해시코드 계산에서 제외해도 된다. 즉, 다른 필드로부터 계산해낼 수 있는 필드는 모두 무시해도 된다.
- equals 비교에 사용되지 않은 필드는 '반드시' 제외해야 한다. 그렇지 않으면 hashCode 규약 두 번째를 어기게 될 위험이 있다.
[작동 원리]
- 31 * result는 필드를 곱하는 순서에 따라 result 값이 달라지게 한다.
- 클래스에 비슷한 필드가 여러 개일 때 해시 효과를 크게 높여준다.
- 예컨대 String의 hashCode를 곱셈 없이 구현한다면 모든 아나그램(anagram, 구성하는 철자가 같고 그 순서만 다른 문자열)의 해시코드가 같아진다.
- 곱할 숫자를 31로 정한 이유는 31이 홀수이면서 소수(prime)이기 때문이다. 만약 이 숫자가 짝수이고 오버플로가 발생한다면 정보를 잃게 된다. 2를 곱하는 것은 시프트 연산과 같은 결과를 내기 때문이다.
- 31을 이용하면 이 곱셈을 시프트 연산과 뺄셈으로 대체해 최적화할 수 있다. 요즘 VM들은 이런 최적화를 자동으로 해준다.
31 * i → (i << 5) - i
[다른 방법]
- 해시 충돌이 더욱 적은 방법을 꼭 써야 한다면 구아바의 com.google.commom.hash.Hashing을 참고하자.
- Object 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 정적 메서드인 hash를 제공한다.
-
@Override public int hashCode() { return Objects.hash(lineNum, prefix, areaCode); }
- 아쉽게도 속도는 더 느리다.
- 입력 인수를 담기 위한 배열이 만들어지고, 입력 중 기본 타입이 있다면 박싱과 언박싱도 거친다.
- hash 메서드는 성능이 민감하지 않은 상황에서만 사용하자.
-
- 클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기보다는 캐싱하는 방식을 고려해야 한다.
- 객체가 주로 해시의 키로 사용될 것 같다면 인스턴스가 만들어질 때 해시코드를 계산해둬야 한다.
- 해시의 키로 사용되지 않는 경우라면 hashCode가 처음 불릴 때 계산하는 지연 초기화(lazy initialization) 전략도 괜찮다.
-
private int hashCode; // 자동으로 0으로 초기화된다. @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; }
- 필드를 지연 초기화하려면 그 클래스를 스레드 안전하게 만들도록 신경 써야 한다아이템83.
-
[주의 사항]
- hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자.
- 그래야 클라이언트가 이 값에 의지하지 않게 된다.
- 또한 추후에 계산 방식을 바꿀 수도 있다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 13] clone 재정의는 주의해서 진행하라 (0) | 2022.01.08 |
---|---|
[아이템 12] toString을 항상 재정의하라 (0) | 2022.01.08 |
[아이템 10] equals는 일반 규약을 지켜 재정의하라 (0) | 2022.01.08 |
[아이템 9] try-finally보다는 try-with-resources를 사용하라 (0) | 2022.01.08 |
[아이템 8] finalizer와 cleaner 사용을 피하라 (0) | 2022.01.08 |