equals를 재정의한 클래스 모두에서 hashCode도 재정의해야한다. 그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킨다.
[ Object 명세에서 발췌한 규약 ]
1. equals 비교에서 사용되는 정보가 변경되지 않았다면, 어플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.
2. equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
3. equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.
논리적으로 같은 객체는 같은 해시코드를 반환해야 한다. 예를 들어 아래와 같은 원소를 사용한다고 가정해보자.
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "수지");
m.get(new PhoneNumber(707, 867, 5309));
위 코드에서 get 메서드를 사용하면 "수지"가 나와야할 것 같지만 실제로 null을 반환한다. 여기서는 2개의 PhoneNumber 객체가 사용됐다. PhoneNumber 클래스는 hashCode를 재정의하지 않았기 때문에 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하여 두 번째 규약을 지키지 못한다.
이 문제는 hashCode 메서드만 작성해주면 해결되며, 좋은 hashCode를 작성하는 간단한 요령은 다음과 같다.
[ hashCode 작성 요령 ]
1. int 변수 result를 선언한 후 값 c로 초기화한다. 이때 c는 해당 객체의 첫 번째 핵심 필드를 단계 2-a 방식으로 계산한 해시코드다.
2. 해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행한다.
a. 해당 필드의 해시코드 c를 계산한다.
a1. 기본 타입 필드라면, Type.hashCode(f)를 수행한다. 여기서 Type은 해당 기본 타입의 박싱 클래스다.
a2. 참조 타입 필드면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 이 필드의 hashCode를 재귀적으로 호출한다. 필드의 값이 null이면 0을 사용한다.
a3. 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다. 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.
b. 단계 2-a에서 계산한 해시코드 c로 result를 갱신한다.
result = 31 * result + c;
3. result를 반환한다.
이러한 단계를 코드로 구현하면 다음과 같다.
@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, prefix, areaCode);
}
클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기보다는 캐싱하는 방식을 고려해야 한다. 이 타입의 객체가 주로 해시의 키로 사용될 것 같다면 인스턴스가 만들어질 때 해시코드를 계산해둬야 한다.
해시의 키로 사용되지 않는 경우라면 hashCode가 처음 불릴 때 계산하는 지연 초기화 전략이 있다. 대신 그 클래스를 스레드 안전하게 만들도록 신경써야 한다 (Theard-safety)
@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;
}
'Developer's_til > Effective Java' 카테고리의 다른 글
[item 14] Comparable을 구현할지 고려하라 (0) | 2022.10.25 |
---|---|
[item 13] 재정의는 주의해서 진행하라 (0) | 2022.10.24 |
[item 10] equals는 일반 규약을 지켜 재정의하라 (0) | 2022.10.13 |
[item 6] 불필요한 객체 생성을 피하라 (2) | 2022.10.03 |
[item 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2022.10.03 |