equals
를 재정의한 클래스 모두에서 hashCode
도 재정의해야 한다.
그렇지 않으면, hashCode
일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap
이나 HashSet
같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.
equals
비교에 사용되는 정보가 변경되지 않았으면, 애플리케이션이 실행되는 동안 그 객체의 hashCode
메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. 단, 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.equals(Object)
가 두 객체를 같다고 판단했다면, 두 객체의 hashCode
는 똑같은 값을 반환해야 한다.equals(Object)
가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode
가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시 테이블의 성능이 좋아진다.hashCode
재정의를 잘못했을 때 크게 문제가 되는 조항은 두 번째이다.
즉, 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
equals
를 재정의한 PhoneNumber
클래스의 인스턴스를 HashMap
의 원소로 사용한다고 해보자.
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "제니");
m.get(new PhoneNumber(707, 867, 5309));
// 위의 행을 실행하면 "제니"가 반환되어야 할 것 같지만 실제로는 null을 반환한다.
위와 같은 결과가 나오는 이유는, PhoneNumber
클래스는 hashCode
를 재정의하지 않았기 때문에 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하여 두 번째 규약을 지키지 못하기 때문이다.
⇒ get
메서드는 엉뚱한 해시 버킷에 가서 객체를 찾으려 한 것이다.
설사 두 인스턴스를 같은 버킷에 담았더라도 get
메서드는 여전히 null
을 반환하는데, HashMap
은 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않도록 최적화되어 있기 때문이다.
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
}