Enumeration에 대한 관찰에서 Enum에 대해 알아보았다. 이번에는 Enum을 효율적으로 사용 하는 방법 중 하나인 EnumMap을 살펴보자.
EnumMap은 Enum을 키로 사용하는 Map이다.
enum DayOfWeek {
MON, TUE, WED, THU, FRI, SAT, SUN
}
Map<DayOfWeek, String> scheduleMap = new EnumMap<>(DayOfWeek.class);
EnumMap은 AbstractMap의 구현체이기 때문에, 일반적으로 알고있는 Map의 기능들을 모두 사용 할 수 있다.
scheduleMap.put(DayOfWeek.MON, "Study");
scheduleMap.get(DayOfWeek.MON);
실질적인 사용법은 일반적인 HashMap등의 다른 Map 구현체와 큰 차이가 없다.
모든 연산을 다루기에는 내용이 많으니, put과 get 연산만 비교해보도록 하자.
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
EnumMap의 put 연산은 해당 enum의 "선언순서"를 인덱스로 사용해 배열에 저장하는 방식으로 동작한다.
만약 동일한 키로 다른 값을 삽입한다면, 이전 데이터는 제거된다.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
코드의 길이부터 확연하게 차이나는것을 알 수 있다.
HashMap 또한 내부적으로 배열인 HashTable이 있다. 키의 hash값을 계산해서 테이블의 인덱스로 사용한다. 만약 해당 인덱스가 비어있다면, 그대로 값을 추가하고, 비어있지 않다면 연결리스트처럼 다음 노드로 연결하여 저장한다.
만약 이렇게 연결된 노드의 갯수가 TREEIFY_THRESHOLD(8로 지정되어있다.)를 넘어서게 되면, 연결리스트가 아닌 Tree형태로 변경되어 저장된다.
이보다 자세한 내용은 추후 HashMap 관련 글에서 다루도록 하겠다.
public V get(Object key) {
return (isValidKey(key) ? unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}
단순히 해당 키의 ordinal을 인덱스로 사용해 배열에서 값을 꺼내 리턴한다.
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
Put 연산에서 살펴보았듯이, 테이블에서 해시값을 인덱스로 노드를 가져오고, 노드의 키가 일치할때까지 연결리스트 또는 트리를 탐색한다.
EnumMap을 보면 Map 인터페이스를 직접 상속 받지 않지만, 생성할 때는 EnumMap 타입과 Map 타입 두가지로 생성 할 수 있다.
EnumMap<DayOfWeek ,String> scheduleMap = new EnumMap<>(DayOfWeek .class);
scheduleMap.put(DayOfWeek.MON, "Study");
scheduleMap.get(DayOfWeek.MON);
Map<DayOfWeek ,String> scheduleMap2 = new EnumMap<>(DayOfWeek .class);
scheduleMap2.put(DayOfWeek.MON, "Study");
scheduleMap2.get(DayOfWeek.MON);
EnumMap으로 생성한 경우는 put과 get연산이 invokevirtual로, Map으로 생성한 경우는 invokeinterface로 호출된다.
그 이유는 EnumMap의 조상인 AbstractMap이 Map 인터페이스를 상속받기 때문이다. 따라서 Map으로 만들어도 EnumMap의 put, get 연산이 호출되니 걱정할 필요는 없다.
Enum은 컴파일 시점에 전체 요소의 개수를 계산할 수 있고, 생성자를 제공하지 않으므로, 이 개수가 변경되지 않는다.
따라서 EnumMap은 데이터의 관리에 배열을 사용할 수 있고, 부수적인 자료형 또한 필요하지 않으므로 메모리를 효율적으로 사용 할 수 있다.
값에 접근할때 인덱스로 직접 배열에 접근하므로 CRUD 모두 O(1)의 시간 복잡도를 가지므로 매우 효율적인 자료구조라 볼 수 있다.
Map을 사용하는데 Key가 Enum이라면 EnumMap의 도입을 반드시 고려해보자