
처음 자바를 공부했을 때, hashCode는 컬렉션에 쓰이는 해시 값을 계산하는 용도, equals는 동등성을 비교하는 용도로 쓰인다 정도로만 이해하고 넘어갔었다.
동등성(equality): 동일한 정보를 가지고 있는지(논리적으로 동일.
equals로 비교)
동일성(identity): 실제로 같은 객체인지(물리적으로 동일. 같은 메모리 주소를 가리킨다.==로 비교)
그러다보니 HashMap 같은 자료구조에서 객체를 키로 사용할 때, hashCode만 같다면 같은 객체를 가리키겠거니 싶어서 hashCode만 오버라이딩하면 된다고 생각했었는데 최근 다시 개념을 정리하면서 큰 착각을 하고 있었다는 걸 알게 되었다.
결론부터 말하면, HashXXX 형태의 컬렉션에서는 객체의 동등성에 따라 값을 저장하고 검색하는데, 객체가 동등하다면 hashCode와 equals가 반환하는 값이 서로 같아야 한다.
equals도 필요한 걸까?hashCode를 재정의하는 것 만으로, hash 자료구조에서 객체를 식별할 수 있는데 왜 equals도 같아야 하는걸까?
그 이유는 값이 충돌할 수 있기 때문이다. 값을 정해진 범위로 매핑하는 hash 의 특성 상, 동등하지 않은 객체가 같은 hashCode를 지닐 수 있다. 그러니까 어쩌다가 우연히, 다른 객체임에도 불구하고 hashCode가 같을 수 있는 것이다.
그래서 해시 기반의 컬렉션(HashMap, HashSet, Hashtable)에서는 먼저 hashCode로 객체가 존재하는 버킷을 찾고, 진짜 이 객체가 맞는지 확인하기 위해 버킷 내부의 LinkedList를 순회하면서 equals 메서드로 동등한 객체를 찾는다.
따라서 값을 추가할 때 hashCode가 같은데 equals 값이 다르다면, 해당 컬렉션에서는 우연히 hashCode가 겹친 다른 객체라고 판단하게 되므로 해당 값을 새로 저장하게 된다.
class Member {
private int id;
public Member(int id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
// equals를 재정의 하지 않았기 때문에 Object.equals 가 실행되고,
// Object.equals는 객체의 메모리 주소를 비교하기 때문에 재정의하지 않는다면 equals는 항상 `false`다.
// public boolean equals(Object obj) {
// return (this == obj);
// }
}
Set<Member> members = new HashSet<>();
members.add(new Member(1));
members.add(new Member(1));
members.size(); // 1이 아니라 2로 나온다.
정리하자면, 동등성 판단이 필요하다면 equals와 hashCode를 반드시 오버라이딩 해야 한다. 그리고 동등성 비교에 대해서는 다음과 같은 원칙을 생각하면 좋다.
equals가 참이라면 반드시 hashCode도 같아야 한다.Hash 기반의 자료구조에서 객체를 찾을 수 없을 것이다.equals가 거짓이라면 hashCode는 다른 것이 좋다.hashCode가 다름을 보장할 순 없기 때문에 다른 것이 "좋다"지만, 가능하면 "달라야 한다". 그렇지 않으면 해시 충돌로 인해 불필요한 연산이 발생할 수 있다.