HashMap을 어느 정도 써봤다면 한 번쯤 이런 경험이 있을 겁니다.
“분명 put으로 값을 넣었는데, get을 하면 null이 나온다”, “같은 객체인데 key로 인식이 안 된다” 같은 상황 말이죠.
이 문제의 거의 대부분은 equals()와 hashCode()를 제대로 이해하지 못해서 발생합니다. 이 글에서는 HashMap에서 equals()와 hashCode()가 왜 중요한지, 그리고 실제로 어떤 문제가 주로 발생하는지 중심으로 정리해보겠습니다.
01. HashMap에서 Key는 어떻게 저장되고 찾을까?
HashMap은 단순히 key를 비교해서 값을 찾지 않습니다. 내부적으로는 다음 순서로 동작합니다.
- key의 hashCode()를 호출한다
- hashCode 값을 이용해 저장 위치(버킷)를 결정한다
- 같은 위치에 key가 여러개 있다면 equals()로 비교한다
따라서, HashMap에서 Key로 객체를 사용할 때는 hashCode()와 equals()가 반드시 함께 올바르게 구현되어야 합니다.
02. equals()만 구현하면 안 되는 이유
만약 equals()만 오버라이드하고 hashCode()는 그대로 두는 실수를 한다면 아래 예제와 같이 equals()는 true인데도 값이 나오지 않습니다. 왜냐하면 hashCode()가 다르기 때문입니다.
class Main {
public static void main(String[] args) {
HashMap<User, String> map = new HashMap<>();
User u1 = new User("admin");
User u2 = new User("admin");
map.put(u1, "관리자");
System.out.println(map.get(u2)); // 실행결과: null
}
}
class User {
String id;
User(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
}
03. equals()와 hashCode()의 계약(Contract)
자바에서는 다음 규칙을 반드시 지켜야 합니다.
- equals()가 true라면 hashCode()도 반드시 같아야 한다
- equals()가 false여도 hashCode()는 같을 수 있다
이제 위에서 언급한 HashMap의 내부동작을 좀 더 살펴보도록 하겠습니다.
HashMap은 두 객체가 서로 같은 객체인지 비교하기 위해 먼저 hashCode()를 실행해 해시코드값이 같은지를 비교합니다. 이때 해시코드값이 다르면 서로 다른 객체로 판단하고, 해시코드값이 같다면 equals()를 통해 다시 한번 두 객체가 같은 객체인지 비교합니다. 그리고 이 두 결과가 모두 맞을때만 서로 같은 객체로 판단합니다.
따라서, (hashCode로 먼저 위치를 찾기 때문에) hashCode가 다르면 equals를 비교할 기회조차 얻지 못하고 서로 다른 객체로 인식하게됩니다.
💡 잠깐! 해시값이 같으면 무조건 같은 객체일까요? 아니요! 서로 다른 객체라 하더라도 같은 해시값을 갖게 될 수도 있습니다. 이를 해시충돌(Hash Collision)이라고 합니다. HashMap은 해시값이 같더라도 equals()를 통해 진짜 같은 객체인지 다시 확인합니다.
04. 올바른 구현 방법
equals와 hashCode를 함께 구현한 예제입니다.
class Main {
public static void main(String[] args) {
HashMap<User, String> map = new HashMap<>();
User u1 = new User("admin");
User u2 = new User("admin");
map.put(u1, "관리자");
System.out.println(map.get(u2)); // 실행결과: 관리자
}
}
class User {
String id;
User(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
💡equals에서 사용한 필드는 hashCode에서도 반드시 동일하게 사용해야 합니다. 그렇지 않으면 HashMap에서 정상적으로 동작하지 않습니다.
05. String은 왜 HashMap Key로 안전할까?
String이 HashMap의 key로 자주 쓰이는 이유는
- equals와 hashCode가 이미 올바르게 구현되어 있음
- 불변 객체(immutable)라 값이 변경되지 않음
반대로, 값이 변경되는 객체를 key로 사용하는 것은 매우 위험합니다. key의 상태가 바뀌면 hashCode도 바뀌어버리기 때문입니다.
User user = new User("admin");
map.put(user, "관리자");
user.id = "guest";
map.get(user); // null
06. 마무리: 주의사항!
- HashMap Key로 객체를 쓸 땐 equals()와 hashCode()는 세트로 구현해야합니다.
- equals()만 구현하면 100% 문제 발생합니다.
- 불변 객체가 가장 안전합니다.
- “put 했는데 get이 안되는 문제”의 대부분은 여기에서 시작됩니다.
HashMap 관련 버그의 상당수는 로직 문제가 아니라 equals()와 hashCode() 설계에서 시작됩니다.!
cf) HashMap 사용방법은 아래 링크의 글을 참고하세요
[Java] HashMap 사용방법과 꼭 기억해야 할 주의사항 (개념, 특징, 메소드 및 예제)
자바 개발에서 HashMap은 가장 기본이면서도, 실무에서는 의외로 많은 문제가 발생하는 컬렉션입니다.간단한 예제에서는 잘 동작하지만, 실제 서비스 코드에서는 “값이 왜 바뀌었는지 모르겠다
kadosholy.tistory.com
'Java' 카테고리의 다른 글
| [Java] ArrayList 사용방법과 꼭 알아야 할 주의사항 (개념, 특징, 메소드 및 예제) (0) | 2026.02.01 |
|---|---|
| [Java] HashMap vs ConcurrentHashMap 차이점 총정리 (언제 무엇을 써야 할까?) (0) | 2026.01.29 |
| [Java] HashMap 사용방법과 꼭 알아야 할 주의사항 (개념, 특징, 메소드 및 예제) (0) | 2026.01.27 |
| [Java] 자바 설치 및 환경변수 등록하기 (OpenJDK 버전별 다운로드) (1) | 2025.08.02 |
| [Java] 자바 - 멀티채팅 프로그램 구현하기 (0) | 2022.08.10 |
