equals를 재정의한 클래스는 hashCode도 재정의해야 한다.
그렇지 않으면 hashCode 일반 규약을 어기게 되어 HashMap나 HashSet같은 컬렉션의 원소로 사용할 때 문제가 발생한다.
hashCode 일반 규약
1. equals 비교에 사용되는 정보가 변경되지 않았다면, hashCode도 변하면 안 된다.
2. equals가 두 객체가 같다고 판단하면, 두 객체의 hashCode는 똑같은 값을 반환한다. ⭐
👉 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
3. equals가 두 객체를 다르다고 판단하더라도, hashCode는 꼭 다를 필요는 없다.
(but 다른 객체일 경우, 다른 값을 반환해야 해시테이블의 성능이 좋아진다)
2번 규약 : 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
equals는 물리적으로 다른 두 객체를 논리적으로는 같다고 할 때, hashCode는 서로 다른 값을 반환한다.
아래 코드는 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하여 null을 반환한다. HashMap은 해시코드가 다를 경우, 동치성 비교를 하지 않기 때문이다.
Map<PhoneNumber, String> map = new HashMap<>();
map.put(new PhoneNumber(010,1234,5678), new Person("제니"));
map.get(new PhoneNumber(010,1234,5678)); // null
그렇다고 모든 객체가 똑같은 해시코드를 반환하는 코드는 적합하지 않다. 시간 복잡도가 늘어나므로..
@Override
public int hashCode() {
return 42;
}
그래서
3번 규약 : 좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환한다
이 요구되는 것이다.
좋은 hashCode를 작성하는 꿀팁
1. int 변수 result를 선언한 후 값 c로 초기화한다.
(c는 해당 객체의 equals 비교에 사용되는 필드를 2단계 a 방식으로 계산한 해시코드다.)
2. 해당 객체의 나머지 핵심 필드인 f 각각에 대해 다음 작업을 수행한다.
a. 해당 필드의 해시코드 c를 계산한다.
i. 기본 타입 필드라면 Type.hashCode(f)를 수행한다. (Type은 해당 기본 타입의 박싱 클래스)
ii. 참조 타입 필드면서 이 클래스의 equals 메소드가 이 필드의 equals를 재귀적으로 호출하여 비교한다면, 이 필드의 hashCode를 재귀적으로 호출한다.
iii. 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다. 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.
b. 2단계 a에서 계산한 해시코드 c로 result를 갱신한다. (result = 31 * result + c)
3. result를 반환한다.
주의할 점
- AutoValue 어노테이션을 통해 테스트를 작성하자.
- 파생 필드(다른 필드로부터 계산할 수 있는 필드)는 해시코드 계산에서 제외해도 된다.
- 반드시 equals 비교에 사용되는 필드만 해시코드를 계산한다. 그렇지 않으면 2번 규약을 어기게 될 수 있다.
- 참조 타입 필드가 null일 경우 0을 사용한다.
전형적인 hashCode 메소드
동치인 인스턴스는 서로 같은 해시코드를 가질 것은 분명하다.
@Override
public int hashCode() {
int result = Integer.hashCode(areaCode);
result = 31 * result + Integer.hashCode(prefix);
result = 31 * result + Integer.hashCode(lineNum);
return result;
}
Objects 클래스의 hashCode 메소드
매우 간단하지만 속도가 느려 성능이 좋지 않다.
cuz 입력 인수를 담기 위한 배열이 만들어지고, 입력 중 기본 타입이 있다면 박싱과 언박싱을 거쳐야 하기 때문
@Override
public int hashCode() {
return Objects.hash(lineNum,prefix,areaCode);
}
hashCode의 캐싱과 지연 초기화
: 클래스가 불변이고 해시코드를 계산하는 비용이 클 경우 적합
필드를 지연 초기화하려면 그 클래스가 thread-safe가 되도록 동기화에 신경 써야 한다.(아이템 83)
동시에 여러 스레드가 hashCode를 호출하면 의도하는 값과 달라질 수 있으니 동기화에 신경 써줘야 한다.
private int hashCode;
@Override
public int hashCode() {
int result = hashCode; // 초기값 0을 가진다.
if(result == 0) {
int result = Integer.hashCode(areaCode);
result = 31 * result + Integer.hashCode(areaCode);
result = 31 * result + Integer.hashCode(areaCode);
hashCode = result;
}
return result;
}
요약
equals를 재정의할 때는 hashCode도 반드시 정의해야 한다. hashCode의 일반 규약을 잘 따라야 하며, 되도록이면 서로 다른 인스턴스일 경우 해시코드도 다르게 구현하자.
마찬가지로 AutoValue 프레임워크를 사용하면 자동으로 equals와 hashCode를 자동으로 만들어준다.
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 13. clone 재정의는 주의해서 진행하라. (0) | 2023.03.22 |
|---|---|
| [effective java] 아이템 12. toString을 항상 재정의하라. (0) | 2023.03.09 |
| [effective java] 아이템 10. equals는 일반 규약을 지켜 재정의하라. (0) | 2023.03.09 |
| [effective java] 아이템 9. try-finally보다는 try-with-resources를 사용하라. (0) | 2023.03.08 |
| [effective java] 아이템 8. finalizer와 cleaner 사용을 피하라. (0) | 2023.03.08 |