
“충돌이 많아지면 Tree로 바뀐다”
하지만 이 문장은 너무 많은 걸 생략한다.
이 글에서는 HashMap 소스 코드에 실제로 등장하는 상수와 주석을 기반으로, 충돌 → 성능 문제 → 트리화(Treeify)까지의 흐름을 정리한다.
HashMap에서 충돌이란,
서로 다른 key들이 같은 버킷(table의 같은 index)에 매핑되는 상황
을 의미한다.
HashMap의 기본 구조는 다음과 같다.
Node<K,V>[] table;
index는 개념적으로 다음과 같이 계산된다.
index = hash & (table.length - 1);
key는 많고, table 길이는 유한하기 때문에 충돌은 필연적으로 발생한다.
중요한 점은 이것이다.
충돌은 오류가 아니라 정상 동작이다.
HashMap은 충돌을 전제로 설계된 자료구조다.
충돌이 발생한 버킷은 이런 구조가 된다.
table[i] → NodeA → NodeB → NodeC → ...
HashMap의 핵심 연산은 get(key)다.
map.get(key);
내부 흐름은 다음과 같다.
여기서 성능이 갈린다.
즉,
버킷에 Node가 많아질수록 HashMap의 탐색 성능은 O(1)에서 O(n)으로 무너진다.
이 순간 HashMap은 “빠른 Map”이 아니라 그냥 LinkedList 탐색기가 된다.
HashMap 소스 주석에는 이런 의도가 명확히 드러난다.
performance degrades gracefully under accidental or malicious usages in which hashCode() methods return values that are poorly distributed
즉,
👉 모든 Node가 하나의 버킷에 몰릴 수 있다.
이걸 방치하면:
그래서 다음 단계(트리화) 가 필요해진다.
HashMap 소스에는 다음과 같은 상수들이 있다.
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
이 숫자들은 전부 충돌과 성능 문제를 제어하기 위한 장치다.
HashMap은 아무 때나 트리화하지 않는다.
다음 두 조건을 동시에 만족할 때만 Tree로 바꾼다.
이 조건은 소스 코드 주석에 그대로 드러나 있다.
/*
* The bin count threshold for using a tree rather than list for a bin.
*/
static final int TREEIFY_THRESHOLD = 8;
/*
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
8은 “마법의 숫자”가 아니다.
리스트 탐색과 트리 탐색의 비용 차이가 확실하게 벌어지기 시작하는 지점이다.
그래서 자바는 이렇게 판단한다.
“이제는 리스트로 버티는 게 손해다”
그 기준점이 8이다.
여기서 중요한 질문이 나온다.
“Node가 8개 이상이면 무조건 충돌 문제일까?”
답은 아니다.
자주 발생하는 상황은 이거다.
이 경우:
그래서 HashMap은 이렇게 행동한다.
table이 충분히 커지기 전(capacity < 64)에는
트리화하지 말고 resize부터 하라
즉,
HashMap 소스 주석에는 이런 설명도 있다.
/*
* Because TreeNodes are about twice the size of regular nodes,
* we use them only when bins contain enough nodes to warrant use
*/
TreeNode는:
그래서 정말 필요할 때만 사용한다.
HashMap의 충돌 대응은 단계적이다.
이를 한 문장으로 정리하면 다음과 같다.
HashMap은 평균 성능과 최악 성능 사이에서 비용을 최소화하는 방향으로 점진적으로 구조를 강화하는 자료구조다.