Java에서 Hash 종류의 Collection을 사용할 때 `hashCode`와 `equals`를 둘 다 오버라이딩 해야 하는 이유는 뭘까?

저니·2023년 6월 9일
0

개요

처음 자바를 공부했을 때, hashCode는 컬렉션에 쓰이는 해시 값을 계산하는 용도, equals는 동등성을 비교하는 용도로 쓰인다 정도로만 이해하고 넘어갔었다.

동등성(equality): 동일한 정보를 가지고 있는지(논리적으로 동일. equals로 비교)
동일성(identity): 실제로 같은 객체인지(물리적으로 동일. 같은 메모리 주소를 가리킨다. ==로 비교)

그러다보니 HashMap 같은 자료구조에서 객체를 키로 사용할 때, hashCode만 같다면 같은 객체를 가리키겠거니 싶어서 hashCode만 오버라이딩하면 된다고 생각했었는데 최근 다시 개념을 정리하면서 큰 착각을 하고 있었다는 걸 알게 되었다.

결론부터 말하면, HashXXX 형태의 컬렉션에서는 객체의 동등성에 따라 값을 저장하고 검색하는데, 객체가 동등하다면 hashCodeequals가 반환하는 값이 서로 같아야 한다.

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로 나온다.

결론

정리하자면, 동등성 판단이 필요하다면 equalshashCode를 반드시 오버라이딩 해야 한다. 그리고 동등성 비교에 대해서는 다음과 같은 원칙을 생각하면 좋다.

  • equals가 참이라면 반드시 hashCode도 같아야 한다.
    그렇지 않다면 Hash 기반의 자료구조에서 객체를 찾을 수 없을 것이다.
  • equals가 거짓이라면 hashCode는 다른 것이 좋다.
    해시 함수의 특성 상 반드시 hashCode가 다름을 보장할 순 없기 때문에 다른 것이 "좋다"지만, 가능하면 "달라야 한다". 그렇지 않으면 해시 충돌로 인해 불필요한 연산이 발생할 수 있다.

0개의 댓글