equals를 재정의하려거든 hashCode도 재정의하라
equals를 재정의한 클래스는 모두 hashCode도 재정의해야 한다. 그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap
이나 HashSet
같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), new Person("제니"));
// m.get(new PhoneNumber(707, 867, 5309)) 실행 시 null 반환
위의 코드에서 PhoneNumber 클래스는 해시코드를 재정의하지 않았기에 논리적 동치인 두 객체가 서로 다른 해시코드를 반환해 두 번째 규약을 지키지 못하고 있다. 두 개의 PhoneNumber 인스턴스를 같은 버킷에 담았더라도 get 메서드는 여전히 null을 반환하는데, HashMap은 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않도록 최적화되어 있기 때문이다.
다음과 같이 hashCode를 재정의할 수도 있다.
@Override
public int hashCode() {
return 42;
}
하지만 위의 경우모든 객체가 해시테이블의 버킷 하나에 담겨 연결 리스트처럼 동작하게 된다. 그 결과 평균 수행 시간이 O(1)인 해시테이블이 O(n)으로 느려져서 객체가 많아지면 쓸 수 없게 된다.
a. 해당 필드의 해시코드 c 를 계산한다.
b. 단계 2.a에서 계산한 해시코드 c로 result를 갱신한다.
result = 31 * result + c;
@Override
public int hashCode() {
int result = Integer.hashCode(areaCode);
result = 31 * result + Integer.hashCode(prefix);
result = 31 * result + Integer.hashCode(lineNum);
return result;
}
위 메서드는 핵심 필드 3개를 사용해 간단한 계산을 수행한다. 비결정적 요소는 없어, 동치인 PhoneNumber 인스턴스들은 같은 해시코드를 가질 것이다.
@Override
public int hashCode() {
return Objects.hash(lineNum,prefix,areaCode);
}
앞의 코드와 비슷한 수준의 hashcode를 한 줄로 작성할 수는 있지만, 속도는 더 느리다.
클래스가 불변이고 해시코드가 계산하는 비용이 크다면, 매번 새로 계산하기보다는 캐싱하는 방식을 고려해야 한다.
이 타입의 객체가 주로 해시의 키로 사용된다면 인스턴스가 만들어질 때 해시코드를 계산해둬야 한다. 해시의 키로 사용되지 않는 경우라면 hashcode가 처음 불릴 때 계산하는 지연 초기화전략이 좋을 것이다.
//캐싱하는 방법, 동기화 신경써야 한다.
private int hashCode; //자동으로 0으로 초기화
@Override
public int hashCode() {
int result = hashCode;
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도 반드시 재정의해야 한다. 또한 AutoValue
프레임워크에서 equals, hashCode를 만들어주니 참고하자.