본문으로 건너뛰기
개발 머꼬
개발 노트Java
hohyeon.dev19

equals()만 오버라이드했다가 HashMap에서 값을 못 찾은 이유

  • #Collections
  • #Common Pitfall
  • #Engineering Note
  • #Java

문제 발생

equals()를 재정의해 두 Point 객체가 좌표만 같으면 같다고 판단하도록 만들었는데, HashSet에 넣은 뒤 논리적으로 같은 객체로 contains()를 호출하면 false가 나왔습니다.

public class Point {
    int x, y;
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }
    // hashCode()는 재정의하지 않음
}

Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
points.contains(new Point(1, 2)); // false — 기대와 다름

원인 분석

HashMap/HashSet은 객체를 저장할 때 먼저 hashCode()로 어느 버킷(bucket)에 넣을지 정하고, 같은 버킷 안에서 equals()로 실제 동일 객체인지 비교합니다. hashCode()를 재정의하지 않으면 Object의 기본 구현(객체의 메모리 주소 기반)을 그대로 씁니다.

new Point(1, 2)를 두 번 만들면 equals()로는 같다고 판정되지만, 기본 hashCode()는 서로 다른 값을 반환합니다. 그러면 두 객체가 서로 다른 버킷에 들어가므로, contains()가 애초에 equals() 비교를 시도하는 버킷 자체를 잘못 찾아가 항상 "없음"으로 판정됩니다.

Java 공식 문서(Object.hashCode())는 이 계약을 명시합니다: "equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다." 역은 성립하지 않아도 됩니다(다른 객체가 같은 hashCode를 가질 수 있음, 이걸 해시 충돌이라 합니다).

해결 방안

  1. equals()를 재정의하면 반드시 hashCode()도 함께 재정의합니다. equals()에서 비교에 사용한 필드를 hashCode() 계산에도 똑같이 사용해야 합니다.
@Override
public int hashCode() {
    return Objects.hash(x, y);
}
  1. IDE의 자동 생성 기능(equals()/hashCode() 동시 생성)을 쓰면 이 실수를 원천적으로 피할 수 있습니다. Lombok의 @EqualsAndHashCode도 같은 이유로 존재합니다.
  2. Java 16+의 record를 쓰면 equals()/hashCode()/toString()이 필드 기반으로 자동 생성되어 이 계약을 신경 쓸 필요가 없습니다 — 단순 값 객체라면 record를 우선 고려할 만합니다.
  3. 가변 필드를 equals()/hashCode()에 사용하는 것도 피해야 합니다 — HashSet에 넣은 뒤 그 필드를 바꾸면 객체가 있어야 할 버킷과 실제 위치가 어긋나 마찬가지로 못 찾게 됩니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.