처음 자바를 공부했을 때, 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
가 다름을 보장할 순 없기 때문에 다른 것이 "좋다"지만, 가능하면 "달라야 한다". 그렇지 않으면 해시 충돌로 인해 불필요한 연산이 발생할 수 있다.