Java에서 Map은 인터페이스이고, 그런 Map의 구현체 중 하나가 HashMap이다.
데이터를 저장하려면 자료구조가 필요한데, HashMap은 자료구조로 배열(array)을 사용한다.
HashMap은 해싱(Hashing)을 통해 Map 데이터가 저장될 위치의 인덱스를 구한다. (그래서 이름이 HashMap이다)
hey(x)를 해싱함수(function)에 넣어 인덱스(Y)를 산출한 후, 해당 인덱스에 Map 데이터를 저장하는 것이 HashMap의 기본 원리이다.
Object
클래스의 hashCode()
메서드가 기본 해시 함수 역할을 한다.해싱은 인덱스를 구하는 것이다. 따라서 아래의 조건을 준수해야 한다.
(n-1) & hash
의 결과를 넣어주는데, 이 식은 hash % n
과 같은 결과를 나타낸다.이처럼 HashMap은 key가 있다면 해싱함수를 통해 바로 해당 인덱스의 위치로 이동할 수 있다.
key를 통해 인덱스 산출 후 데이터에 접근한다면, 시간복잡도는 O(1)이 된다.
즉, 데이터 접근 성능이 매우 뛰어나다.
HashMap 내부의 배열을 버킷(bucket)이라고 부른다.
버킷 안에 저장된 Map 데이터를 자바에서는 Node 객체로 만들어 관리한다.
HashMap
클래스안에 Node 배열을 table이란 이름으로 관리하는 필드를 확인할 수 있다. 이 table이 버킷이다.
버킷안에 저장된 Node객체를 확인할 수 있다.
필드로 hash값, key, value, 그리고 next값을 알 수있다.
해시를 사용하다보면 서로 다른 key가 동일한 인덱스를 부여받는 충돌(collision)이 발생할 수 있다.
충돌은 크게 2가지를 이유로 발생한다.
key는 무한대로 존재할 수 있지만, 인덱스는 배열의 크기보다 작은 정수로 한정되어 있다.
따라서 key 사이의 충돌은 불가피하다.
해시 함수는 유한한 범위의 값을 반환하기 때문에, 충돌을 피할 수 없다.
java의 HashMap은 충돌을 해결하기 위해 체이닝(Chaining) 방식을 사용한다.
equals()
로 정확한 키를 찾음.equals()
가 일치하는 노드를 조회한다버킷에 데이터가 몰리면 검색 속도가 저하된다.
해시 테이블이 꽉 차면 성능이 O(1)에서 O(n)으로 악화될 수 있다.
따라서, 버킷의 데이터가 일정 수준을 넘어가면 사이즈를 조절하는 방법(리사이징)과 버킷의 데이터가 너무 많은 경우 탐색 방법을 변환하는 방법(트리화)을 사용할 수 있다.
버킷의 75%가 채워지면 충돌 확률이 크게 늘어난다.
그래서 HahsMap은 버킷의 입실률이 75%를 넘으면 버킷의 사이즈를 늘린다.
HashMap.java
)
- threshold(
newThr
)
- 버킷의 사이즈를
resize()
할 때의 임계점.- 처음 버킷 생성 시 지정된다.
(int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
DEFAULT_LOAD_FACTOR
: 75%DEFAULT_INITIAL_CAPACITY
: 16, 버킷의 처음 사이즈.newThr = oldThr << 1;
<<
쉬프트 연산자를 이용한다. 버킷의 사이즈를 조절하더라도 충돌은 발생할 수 있다.
Java는 충돌의 횟수에 따라 충돌한 데이터들의 저장방식을 달리한다.
초기에는 앞서 언급한 것 처럼 LinkedList(연결리스트)를 기반으로 작동한다.
Node 객체 내부에 존재하는 next 필드를 따라 조회하며 equals()로 같은 키를 가지는 노드를 탐색한다.
그러나 충돌 횟수가 일정 수치를 넘어가면 LinkedList 방식의 데이터 저장은 효율이 크게 떨어지게 된다.
만약 어떤 Node 객체가 n번 충돌되어 n번째 Node의 next에 저장되면, 해당 Node를 탐색하는데 소요된 시간복잡도가 O(n)이 된다.
충돌이 많아질수록 효율이 떨어지는 것이다.
이를 개선하기 위해 Java 8부터는 일정 충돌 수치를 초과하면 해당 버팃의 데이터 구조를 Red-Black Tree로 변환하여 성능을 O(log n)으로 최적화한다.
따라서, 탐색, 삽입, 삭제 연산의 시간 복잡도는 O(log n)으로 유지된다.
💡 직관적 이해
- LinkedList의 경우: 데이터가 쌓일수록 탐색 시간이 선형적으로 증가 -> O(n)
- 레드-블랙 트리의 경우: 트리의 균형이 유지되어 데이터가 많아도 탐색 경로가 짧음 -> O(log n)
HashMap.java
)binCount
는 충돌의 횟수를 체크한다. TREEIFY-THRESHOLD - 1
을 넝므면 treeifyBin()
을 통해 트리로 전환한다.🚩 문제 | 💡 해결 아이디어 | ⚙️ 설계 결정 |
---|---|---|
빠른 데이터 검색이 필요함 | 해시 함수를 사용해 인덱스 찾기 | hashCode() 로 O(1) 접근 |
해시 충돌 발생 가능 | 체이닝(LinkedList) 또는 오픈 어드레싱 | 체이닝 방식 채택, 충돌 시 연결 리스트 사용 |
충돌 시 키의 정확성 확인 필요 | equals() 로 동등성 비교 | equals() 로 정확한 키 확인 |
데이터 증가로 인한 성능 저하 | 리사이징 및 트리화(Treeify) 적용 | 해시 테이블 확장 + 트리 구조로 최적화 |
"HashMap은 빠른 검색을 위해 해시를 사용하고, 해시 충돌을 체이닝과 트리화로 해결하며,
equals()
로 정확성을 보장하는 구조다."