자료구조와 자바를 공부하다보면
우리는 Hash, HashTable, HashMap 등의 이름을 마주하게 된다.
하지만 자바를 컬렉션에 대해서 더 깊게 공부하다보면
HashTable과 HashMap은 하는 기능은 똑같은 거 같은데 무슨 차이가 있지?
라는 생각이 들게 된다.
오늘은 두 자료구조정확히는 구현체에 대해서 차이점을 자세히 알아보자.
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
// 해시 테이블의 버킷 배열 (트랜지언트, 직렬화되지 않음)
private transient Entry<?,?>[] table;
// 해시 테이블의 항목 수 (트랜지언트, 직렬화되지 않음)
private transient int count;
// 해시 테이블이 확장되는 임계값
private int threshold;
// 로드 팩터
private float loadFactor;
// 구조적 변경 횟수 (트랜지언트, 직렬화되지 않음)
private transient int modCount = 0;
// 직렬화 버전 UID
@java.io.Serial
private static final long serialVersionUID = 1421746759512286392L;
// 초기 용량과 로드 팩터를 사용하는 생성자
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: " + loadFactor);
if (initialCapacity == 0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
...
}
자바로 해시테이블을 구현한 구현체 중에서 가장 오래됨public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
@java.io.Serial
private static final long serialVersionUID = 362498820763181265L;
// 기본 초기 용량 - 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 최대 용량 - 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
// 기본 로드 팩터
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 트리로 변환되는 버킷의 노드 수 임계값
static final int TREEIFY_THRESHOLD = 8;
// 리스트로 변환되는 버킷의 노드 수 임계값
static final int UNTREEIFY_THRESHOLD = 6;
// 트리로 변환되기 위한 최소 테이블 용량
static final int MIN_TREEIFY_CAPACITY = 64;
// 기본 해시 버킷 노드
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); }
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this) return true;
return o instanceof Map.Entry<?, ?> e &&
Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue());
}
}
// 기본 생성자
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 초기 용량과 로드 팩터를 사용하는 생성자
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0 || loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException();
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 해시 함수
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 키와 값을 맵에 삽입
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
...
}
key-value 형태, key는 중복 불가능 등해시 충돌이 상대적으로 적음