핵심 정리
- equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.
- HashSet 혹은 HashMap 같은 컬렉션의 원소로 사용할 경우, 문제를 일으킬 수 있기 때문이다.
- AutoValue 프레임워크를 사용하면 equals, hashCode를 자동으로 만들어 준다.
hashCode를 재정의하지 않으면 발생하는 문제
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "제니");
m.get(new PhoneNumber(707, 867, 5309)); // null
- 위 코드에서 HashMap의 원소를 꺼내면 `null`을 반환한다.
- 이는 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하였기 때문이다.
- PhoneNumber의 `hashCode()`를 재정의하지 않았기 생기는 문제 !
hashCode 재정의 방법
1. 적법하지만 최악의 방법
@Override
public int hashCode() {
return 42;
}
- 동치인 모든 객체에 대해 같은 해시코드를 반환하기 때문에 적법한 코드이다.
- 하지만 이는 치명적인 문제점이 발생하게 된다.
- 모든 객체에 대해 같은 값을 반환하여 해시테이블 버킷 하나에 담겨 수행시간이 O(N)으로 느려지게 된다.
2. 전형적인 hashCode 메서드
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
- 이는 해시 충돌을 줄이기 위해 값을 고르게 섞는 것을 목표로 정의된 코드이다.
- int 변수인 result를 선언한 뒤, 해당 객체의 첫번째 핵심 필드를 계산한 해시코드로 초기화한다.
- 해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행한다.
- 해당 필드의 해시코드 c를 계산한다.
- 기본 타입 필드 - Type.hashCode(f)를 수행한다. 여기서 Type은 기본 타입의 박싱 클래스이다.
- 참조 타입 필드 - 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면 이 필드의 hashCode를 재귀적으로 호출한다.
- 필드가 배열 - 핵심 원소 각각을 별도 필드처럼 다루며, 배열에 핵심 원소가 없다면 상수(0)을 사용한다. 만약 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.
- 단계 2-1에서 계산한 해시코드 c로 result를 갱신한다.
- 해당 필드의 해시코드 c를 계산한다.
3. 성능이 조금 아쉽지만 한 줄로 작성하는 hashCode 메서드
@Override
public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
- Object 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 정적 메서드 hash를 제공한다,
- 이 메서드는 한 줄로 작성할 수 있지만 속도는 두 번째 방식과 비교했을 때 성능이 떨어진다.
- 이는 기본 타입이 있다면 박싱과 언방식을 거쳐야 하기 때문에 발생하는 문제이다.
4. hashCode 계산 비용이 클 경우, 캐싱을 고려한 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;
}
- 클래스가 불변이고, 해시코드를 계산하는 비용이 크다면 캐싱 방식으로 고려해보자.
- 이는 타입의 객체가 주로 해시의 키로 사용될 것 같다며느 인스턴스가 만들어질 때 해시코드를 계산해야 한다.
- 하지만 성능을 높인다고, 해시 코드를 계산할 때 핵심 필드를 생략해서는 안된다. 이는 해시테이블의 성능에 영향을 끼친다.
해시 충돌이 더욱 적은 방법을 사용해야 한다면 구아바를 사용해보자.
'Java > Effective Java' 카테고리의 다른 글
[이펙티브 자바] Item 10. equals는 일반 규약에 지켜 재정의하라 (0) | 2025.03.27 |
---|---|
[이펙티브 자바] Item 9. try-finally보다 try-with-resource를 사용하라 (0) | 2025.03.24 |
[이펙티브 자바] Item 8. finalizer와 cleaner 사용을 피하라 (0) | 2025.03.24 |
[이펙티브 자바] Item 7. 다 쓴 객체 참조를 해제하라. (0) | 2025.03.24 |
[이펙티브 자바] Item 6. 불필요한 객체 생성을 피하라 (0) | 2025.03.24 |