equals를 재정의한 클래스 모두에서 hashCode도 재정의해야한다.
재정의하지 않으면 hashCode 일반 규약을 어겨, HashMap, HashSet과 같은 컬렉션 원소로 사용할 때 문제가 생긴다.
⭐️ 논리적으로 같은 객체는 같은 해시코드를 반환해야한다.
hashCode를 재정의하지 않은 예시
HashMap에 저장할 때 사용된 인스턴스, 조회할 때 사용된 인스턴스가 2개 사용되었다.
둘은 논리적으로 같지만, 물리적으로 다르기 때문에 다른 해시 코드를 리턴하게 된다.
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "제니");
System.out.println(m.get(new PhoneNumber(707, 867, 5309)); // null 반환
잘못된 hasCode 구현방법
모든 객체에게 똑같은 값을 주기 때문에, 연결리스트 처럼 동작한다.
따라서 O(1)인 해시테이블이 O(n) 수준으로 느려진다. 객체가 많다면 더욱 더 느려진다.
@Override public int hashCode() { return 42; }
이상적인 hashCode는 서로 다른 인스턴스에 다른 해시코드를 반환해야한다.
좋은 hasCode 구현방법
1. int 변수를 result로 선언하여 c값으로 초기화한다 (첫 번재 핵심 필드(equals 비교에 사용되는) 를 계산한 해시코드)
2. 나머지 핵심 필드 각각에 아래 작업을 수행한다.
- 1) 해당 필드의 해시코드 c를 계산한다.
- 기본 타입 필드: Type.hashCode(f)
- 참조 타입 필드:
- equals를 재귀적으로 호출하는 경우, hashCode 재귀적으로 호출
- 계산이 복잡해지는 경우, 표준형의 hashCode 사용
- 필드 값이 null인 경우, 0 사용
- 배열: 핵심 원소 각각을 별도 필드처럼 다루기
- 모든 원소가 핵심 원소인 경우, Arrays.hashCode 사용
- 배열에 핵심 원소가 하나도 없는 경우, 상수 사용 (0)
- 일부 원소가 핵심인 경우, result = 31 * result +c 식 사용
- 2) 구한 c를 바탕으로 result를 갱신한다.
- result = 31 * result + c
- 31을 곱하는 이유는 홀수이면서 소수이기 때문에 (오버플로우 방지용)
- 3) result를 반환한다.
이때 아래 필드는 제외한다.
- 파생 필드 (다른 필드로 부터 계산해내는 필드) 무시 가능
- equals 비교에 사용되지 않은 필드는 반드시 제외
성능을 높이기 위해 해시코드 계산 시 핵심 필드를 생략하면 안된다.
속도는 빨라지지만, 해시 품질이 나빠져 테이블 성능을 떨어뜨릴 수 있다.
잘 구현된 hashCode
- 핵심 필드만 사용해 간단한 계산 수행, 비결정적 요소 제외
@Override public int hashCode(){
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
Objects 클래스에서 제공하는 hash 메서드
- 위 코드와 다르게 속도가 느림
- 입력 인수를 담기 위한 배열 생성, 입력 중 기본 타입이 있다면 박싱과 언박싱 과정이 필요하기 때문
- 성능이 민감하지 않은 상황에서만 사용할 것
@Override public int hashCode(){
return Objects.hash(lineNum, preifx, areaCode);
}
지연 초기화하는 hashCode
- 불변 클래스에서 해시 코드 계산 비용이 크다면, 캐싱을 고려해야한다. (해시 코드를 다시 계산할 필요가 없으므로 저장해서 사용)
- 처음 호출될 때만 계산하고, 이후에는 계산 값을 리턴하는 지연 초기화 전략을 사용하자.
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;
}
반응형
'도서 > 이펙티브 자바' 카테고리의 다른 글
| [이펙티브 자바] 12. toString을 항상 재정의하라 (4) | 2025.07.24 |
|---|---|
| [이펙티브 자바] 10. equals는 일반 규약을 지켜 재정의하라 (1) | 2025.07.17 |
| [이펙티브 자바] 9. try-finally 보다는 try-with-resources를 사용하라 (0) | 2025.04.15 |
| [이펙티브 자바] 8. finalizer 와 cleaner 사용을 피하라 (0) | 2025.03.23 |
| [이펙티브 자바] 7. 다 쓴 객체 참조를 해제하라 (0) | 2025.03.21 |