
자바 HashMap은 key로 값을 바로 찾는 자료구조처럼 보이지만, 실제로는 `hashCode()`와 `equals()`가 순서대로 협력합니다.
핵심은 hashCode로 후보 위치를 좁히고 equals로 최종 확인한다는 점입니다. 이 글은 Java 공식 API 문서를 기준으로 HashMap 조회 흐름, collision, mutable key 문제를 코드로 정리합니다.

자바 HashMap 조회를 한 문장으로 보면
HashMap은 모든 key를 처음부터 하나씩 비교하지 않습니다. 먼저 key의 hashCode 값을 이용해 내부 배열의 어느 영역을 볼지 좁힙니다. 그다음 같은 영역에 들어온 후보 key들과 equals로 비교합니다.
Map<UserId, String> names = new HashMap<>();
names.put(new UserId(10), "Kim");
String value = names.get(new UserId(10));위 코드가 제대로 동작하려면 `new UserId(10)` 두 개가 논리적으로 같은 key라는 사실을 `equals()`와 `hashCode()`가 함께 설명해야 합니다.
hashCode는 위치를 정하고 equals는 정체를 확인한다
hashCode는 정확한 정답을 고르는 함수가 아닙니다. 내부적으로 볼 후보 위치를 정하는 힌트에 가깝습니다. 서로 다른 객체가 같은 hashCode를 가질 수 있기 때문에 hashCode만으로 같은 key라고 판단하면 안 됩니다.
- hashCode가 다르면 보통 다른 bucket 후보로 간다
- hashCode가 같아도 서로 다른 key일 수 있다
- 그래서 같은 bucket 안에서는 equals가 필요하다
- equals가 true인 객체는 같은 hashCode를 가져야 한다
collision은 실패가 아니라 해시 테이블의 정상 상황
collision은 둘 이상의 key가 같은 bucket 후보에 모이는 상황입니다. 이것만으로 버그는 아닙니다. HashMap은 같은 bucket 안에서 후보를 더 비교할 수 있도록 구조를 가지고 있습니다.
final class BadKey {
private final int id;
BadKey(int id) { this.id = id; }
@Override public int hashCode() { return 1; }
@Override public boolean equals(Object o) {
return o instanceof BadKey other && id == other.id;
}
}이 예시는 일부러 모든 key의 hashCode를 같게 만듭니다. 동작은 할 수 있지만 후보가 한곳에 몰리므로 HashMap의 장점이 줄어듭니다. 그래서 좋은 hashCode 분포가 중요합니다.
mutable key가 가장 위험한 이유
HashMap에 넣은 뒤 key 객체의 equals/hashCode에 쓰이는 값이 바뀌면 문제가 생깁니다. 저장할 때 계산한 위치와 조회할 때 계산한 위치가 달라질 수 있기 때문입니다.
class UserKey {
String email;
UserKey(String email) { this.email = email; }
@Override public int hashCode() { return Objects.hash(email); }
@Override public boolean equals(Object o) {
return o instanceof UserKey other && Objects.equals(email, other.email);
}
}
UserKey key = new UserKey("a@example.com");
map.put(key, "saved");
key.email = "b@example.com";
System.out.println(map.get(key)); // 찾지 못할 수 있다HashMap key는 가능하면 불변 객체로 두는 편이 안전합니다. Java record나 final field를 사용하면 이런 실수를 줄일 수 있습니다.
실무 체크리스트
- HashMap key로 쓸 클래스는 equals와 hashCode를 함께 검토한다
- equals가 true인 두 객체의 hashCode가 같은지 확인한다
- key로 들어간 뒤 바뀔 수 있는 필드는 비교 기준에서 빼거나 객체를 불변으로 만든다
- 성능 수치를 단정하기보다 key 분포와 collision 가능성을 함께 본다
- record, 값 객체, IDE 생성 코드도 의미 기준이 맞는지 사람이 확인한다
정리
HashMap를 이해할 때는 용어부터 외우기보다 이 도구가 어떤 경계를 나누려는지 먼저 보는 편이 좋습니다. 오늘 글의 기준은 후보를 좁히는 흐름, 반복 상태의 이동, 입력 검증의 책임, 빌드 입력의 변경 빈도, 읽기와 쓰기의 복잡도 차이를 구분하는 것입니다.
함께 보면 좋은 내부 글은 자바 equals hashCode 규약: 왜 같이 구현해야 컬렉션 버그가 줄어들까, 자바 record는 class와 무엇이 다를까, 자바 Stream은 for문보다 항상 좋을까입니다. 외부 기준은 Oracle Java SE 21 – HashMap, Oracle Java SE 21 – Object hashCode/equals, Oracle Java SE 21 – Map를 확인했습니다.