equals를 재정의한 클래스에서는 hashCode도 재정의해야 합니다.
그렇지 않으면 HashMap과 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 발생합니다.
hashCode 규약
- 1) equals 비교에 사용되는 정보가 변경되지 않았다면,애플리케이션이 샐행되는 동안에는 특정 객체의 hashCode를 몇 번을 호출하든 항상 같은 값을 반환해야 한다.
- 2) equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 같은 값을 반환해야 한다.
- 3) equals가 두 객체를 다르다고 판단했더라도 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.
hashCode를 잘못 정의했을 때, 문제가 되는 조항은 두번째입니다.
equals는 논리적으로 다른 두 객체를 같다고 할 수 있는데, 논리적으로 같은 객체는 같은 해시코드를 반환해야 합니다.
그런데 만약 잘못 정의하였다면? hashCode는 서로 다른 값을 반환할 것입니다.
그렇다면 올바른 hashCode 메서드를 구현하는 방법에 대해 알아봅시다.
(밑의 예시들과 함께 보면 이해가 더 수월합니다!)
1) int 변수 result를 선언하고 값 c(객체의 첫번째 핵심 필드(equals에 사용되는 필드))를 2 방식으로 계산한 해시코드)로 초기화한다.
2) 해당 객체의 나머지 핵심 필드인 f 각각에 대해 다음 작업을 수행한다.
2.1) 해당 필드의 해시코드 c를 계산한다.
2.1.1) 기본 타입 필드라면 Type.hashCode(f)를 수행한다.
2.1.2) 참조 타입 필드라면 해당 필드의 hashCode를 재귀적으로 호출한다.
2.1.3) 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다.
이상의 규칙을 재귀적으로 적용해 각 핵심 원소의 해시코드를 계산한 다음 2.2 방식으로 갱신한다.
만약 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.
2.2) 2.1에서 계산한 해시코드 c로 result를 갱신한다. (result = 31 * result + c;)
3) result를 반환한다.
+) equals 비교에 사용되지 않은 필드는 ‘반드시' 제외해야 합니다.
그렇지 않으면 hashCode 두번째 규약을 어길 수도 있습니다.
equals에서 만든 ColorSquare 클래스의 hashCode를 작성해봅시다.
// 전형적인 hashCode 메서드
@Override
public int hashCode() {
int result = Short.hashCode(width);
result = 31 * result + Short.hashCode(height);
result = 31 * result + Short.hashCode(color);
return result;
}
숫자 31을 곱하는 이유는 해당 숫자가 홀수이면서 소수이기 때문입니다.
해당 숫자가 짝수이고 오버플로우가 발생한다면 정보를 잃기 때문입니다. (2의 배수는 비트의 이동을 의미하기 때문.)
또한, 관례적으로 이렇게 해왔습니다.
Object 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 메서드인 hash를 제공합니다.
@Override
public int hashCode() {
return Objects.hash(width, height, color);
}
하지만 이는 입력 인수를 담기 위한 배열이 만들어지고, 입력 중 기본 타입은 박싱과 언박싱도 거쳐야 하므로 성능이 아쉽습니다.
성능이 살짝 아쉽다면, 캐싱과 지연 초기화를 고려해 봅시다.
private int hashCode; // 자동으로 0으로 초기화한다.
@Override
public int hashCode() {
int result = hashCode;
if(result == 0) {
int result = Short.hashCode(width);
result = 31 * result + Short.hashCode(height);
result = 31 * result + Short.hashCode(color);
hashCode = result;
}
return result;
}
해당 타입의 객체가 주로 해시의 키로 사용될 것 같다면, 인스턴스가 생성될 때 해시코드를 계산해 두어야 합니다.
반면, 해시의 키로 사용되지 않을 경우라면 위의 코드처럼 지연 초기화(Lazy Initailization)를 사용할 수 있습니다.
하지만 필드를 지연 초기화하려면 스레드의 안전성까지 고려해야 합니다.
저자의 결론:
equals를 재정의할 때는 hashCode도 반드시 재정의해야 한다.
재정의한 hashCode는 Object의 API문서에 기술된 일반 규약을 따라야 하며, 서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 구현해야 한다.
Reference
조슈아 블로크, 이펙티브 자바 Effective Java 3/E, 2018
'Java' 카테고리의 다른 글
자바 성능 튜닝 끄적끄적 (0) | 2024.02.17 |
---|---|
[Java] Thread Pool (0) | 2022.09.12 |
[Java] 스레드 동기화 - Synchronized (0) | 2022.09.11 |
[Effective java] equals 일반 규약 (0) | 2022.02.17 |