반응형
Java HashMap은 어떻게 동작하는가?(NAVER D2)
- 해시맵과 테이블은 기능은 동일하다
- 다만 보조해시 함수를 사용하는 해시맵이 충돌이 덜 발생할 수 있어 상대적으로 성능상 이슈가 있다.
- 해시 테이블은 거의 변화가 없지만 해시 맵은 버전을 거치면서 변화가 많다
- 어떤 변화?
// 해시맵의 선언부
public class java/util/HashMap extends java/util/AbstractMap implements java/util/Map java/lang/Cloneable java/io/Serializable {
// 해시 테이블의 선언부
public class java/util/Hashtable extends java/util/Dictionary implements java/util/Map java/lang/Cloneable java/io/Serializable {
-
Dictionary 가 하는 역할은?
-
해시맵은 왜 Map을 implements 한 AbstractMap을 extends 하고, Map을 implements 했을까?
-
객체가 나타내려는 값 자체를 해시 값으로 사용할 때는 완전한 해시 함수 , 그러나 String이나 Object는 완전 해시 함수 불가능
-
해시 판별은 각 객체가 hashCode() 메서드가 반환하는 값을 사용함, 길어봐야 자료형 int, 32비트 정수 자료형이다 → 완전 해시 함수 불가능
-
실제 해시 함수의 표현 정수 범위보다 작은 M개의 원소 배열을 사용함
-
int index = X.hashCode() % M → 1/M 확률로 같은 버킷을 사용함
-
충돌이 발생해도 잘 저장하고 조회할 수 있는 대표 방식이 Open Addressing / Separate Chaining
-
Open Addressing : 충돌 → 다음 빈 공간
- 삭제시 문제가 많음, 해시값이 1인 A 가 들어가고 동알한 해시값을 가진 AA 가 삽입될 경우 충돌 발생 → 다음 해시값 2에 넣어둠
- 만약 1인 A를 삭제하고 AA도 삭제하려고 할 경우 AA 해시값인 1 자리에는 비어있게 됨 → 추가로 더미노드를 작성하여 넣어두어야 함
- 해시 밀도가 높아지면 워스트 케이스 발생 빈도가 높아짐
-
Separate Chaining : 충돌 → 선형적으로 리스트를 만듦
- 1.8 이상에서는 8개 이상이면 트리로 구현하여 시간복잡도를 낮춤
-
자바 1.7에서는 버킷의 구현이 아래와 같이 되어 있다.
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
..
// 버킷을 구성하는 Entry(노드) class
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {...}
public final int hashCode() {...}
public final String toString() {...}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {...}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {...}
}
- Separate Chaining 방식을 사용하는 java 7 에서 put 메서드는 아래와 같다.
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null) // hashmap 에서는 키값이 null 가능
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; // 이미 키가 있다면 새로운 값으로 변경하고 이전값을 반환
}
}
modCount++; // ConcurrentModificationException 을 발생시킬지 확인하기위해
// 새로운 값이면 삽입, 이때 threshold와 loadFactor 에 의해서
// 지정된 크기 만큼 커진다면 newCapacity * loadFactor (0.75) 값으로 새롭기 설정하고
// 리사이징이 일어남
addEntry(hash, key, value, i);
return null;
}
- java 8 에서는 조금 다르게 구현함
- Separate Chaining에서 동일 키가 특정 개수 이상이면 링크드 리스트를 대신해서 트리 형식으로 사용함
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
-
또한 Entry 클래스를 대신해서 Node를 사용하는 TreeNode 클래스를 사용함
-
이때 사용하는 방식은 레드 블랙트리
-
해시 크기는 항상 2의 승수로 늘어남
-
그렇기 때문에 index = X.hashCode() % M 공식에서 index는 2의 승수만큼의 비트 개수만 사용하게 됨
-
2의 승수로만 나누면 해시 출동이 쉽게 일어남
-
그래서 보조 해시 함수가 필요함
// java 7 버전 보조 해시 함수
final int hash(Object k) {
// Java 7부터는 JRE를 실행할 때, 데이터 개수가 일정 이상이면
// String 객체에 대해서 JVM에서 제공하는 별도의 옵션으로
// 해시 함수를 사용하도록 할 수 있다.
// 만약 이 옵션을 사용하지 않으면 hashSeed의 값은 0이다.
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// 해시 버킷의 개수가 2a이기 때문에 해시 값의 a비트 값만을
// 해시 버킷의 인덱스로 사용한다. 따라서 상위 비트의 값이
// 해시 버킷의 인덱스 값을 결정할 때 반영될 수 있도록
// shift 연산과 XOR 연산을 사용하여, 원래의 해시 값이 a비트 내에서
// 최대한 값이 겹치지 않고 구별되게 한다.
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 8버전 보조 해시 함수
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
- 이렇게 간단해진 이유는 링크드 리스트 대신에 트리를 쓰는 것과 해시 함수들이 발전한 결과다
반응형
'IT > JAVA' 카테고리의 다른 글
ThreadLocal에 대해서 알아보자 - 1 (0) | 2021.03.12 |
---|---|
Java Garbage Collection (1) | 2020.11.09 |
Filter 를 활용한 ACL 만들기 (feat. Spring) (0) | 2019.11.15 |
Spring Boot Resource 사용시 접두사(classpath, file 등)를 사용해야 하는 이유 (2) | 2019.10.25 |
@Autowired 필드주입 Spring 없이 mock 생성하여 테스트하기 (0) | 2019.10.17 |