해시 컬렉션은 넣는 순간 계산한 해시로 자리(버킷)를 정하고, 그 뒤로는 다시 계산하지 않는다.
무슨 일이 벌어지나
Set<Member> set = new HashSet<>(); // equals·hashCode 가 name 기반
Member m = new Member("철수");
set.add(m); // hash("철수") → 3번 버킷에 저장
m.name = "영희"; // 필드만 바뀐다. 저장 위치는 그대로
set.contains(m); // hash("영희") → 9번 버킷을 뒤진다 → false
set.remove(m); // 같은 이유로 실패
set.size(); // 1 — 못 꺼내고 못 지우는 원소가 남는다
왜 자동으로 재배치되지 않나
컬렉션이 원소의 필드 변경을 알 방법이 없다. 알려면 모든 필드 변경을 감시해야 하는데, 그 비용은 해시 자료구조가 O(1)인 이유 자체를 무너뜨린다. 그래서 자바는 감시 대신 계약을 택했다 — "키는 변하지 않는다."
메모리 관점
버킷 배열의 인덱스는 (n-1) & hash 로 계산된다. 해시가 달라지면 다른 인덱스를 보게 되고,
원소는 여전히 옛 인덱스의 연결 리스트(또는 트리)에 매달려 있다.
탐색은 그 리스트를 아예 방문하지 않으므로 equals 를 비교할 기회조차 없다.
이렇게 피한다
- 키는
record나final필드로 불변으로 만든다 - 가변 객체가 불가피하면 식별자(ID)를 키로 쓰고 객체는 값으로 둔다
- 꼭 바꿔야 한다면
remove→ 수정 →add순서로 꺼냈다 다시 넣는다
면접에서
"HashSet 에 넣은 객체의 필드를 바꾸면 어떻게 되나요"는 자주 나온다. "예외가 나지 않고 조용히 못 찾게 된다" 는 점, 그리고 "반복자로는 여전히 보인다" 는 점까지 말하면 실제로 겪어 본 사람으로 들린다.