Post

Map (HashMap, LinkedHashMap, TreeMap)

Map (HashMap, LinkedHashMap, TreeMap)

  • Map 인터페이스는 Collection 인터페이스와는 다른 저장 방식을 가진다.
  • 자바는 java.util.Map<K,V> 인터페이스를 제공하며 키/값 모두 반드시 참조형이어야한다.

Map<K,V> 인터페이스

  • 키를 값에 매핑하는 객체이며 Map에는 중복 키가 포함될 수 없다.
  • 키-값 쌍을 자바 7까지는 Entry 클래스를 사용하여 표현했다면 8부터는 Node 클래스를 사용한다.
  • 그 구현체로는 HashMap, TreeMap, Hashtable, SortedMap, Collection, Set 등이 있다.

Map 구현체

1. HashMap

  • hashing
    • 객체 데이터를 대표적인 정수 값으로 매핑하는 알고리즘 인 해싱의 원리에 따라 작동한다.
    • 키-값 쌍을 저장, 검색하기 위해 버킷의 인덱스를 계산하기 위해 키 객체에 해싱 함수가 적용된다.
  • bucket
    • HashMap은 내부적으로 bucket을 사용하는데 이 때 요소가 어떤 bucket에 들어가게 될지는 그 요소의 hashCode()값에 의해 결정된다.
  • 동기화
    • 동기화되지 않는다. 여러 스레드가 해시 맵에 동시에 액세스하고 스레드 중 하나 이상이 구조적으로 맵을 수정하는 경우 외부에서 동기화 해야한다.
      • Map m = Collections.synchronizedMap(new HashMap(…));
  • 순서
    • 맵의 순서를 보장하지 않는다.

1-1 HashMap 주요 필드

1
2
3
4
transient Node<K,V>[] table;
    ...
int threshold;
final float loadFactor;
  • transient Node<K,V>[] table
    • transient로 선언된 이유는 직렬화(serialize)할 때 전체, table 배열 자체를 직렬화하는 것보다 키-값 쌍을 차례로 기록하는 것이 더 효율적이기 때문이다.
  • threshold
    • 대략적으로 현재 용량과 부하율의 곱을 의미하며 이 값을 초과하면 Map이 다시 해시하여
  • loadFactor
    • Map 의 용량을 늘릴 시기를 결정하는 척도이다.

1-2 HashMap 생성자

1
2
3
4
5
6
7
8
9
10
11
12
13
public HashMap(int initialCapacity, float loadFactor) {
    ...
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
  • loadFactor를 매개변수로 주지 않을 경우 DEFAULT_LOAD_FACTOR(0.75)를 주입된다.

1-3 HashMap put 메소드

  • 초기화된 HashMap에 put 메소드를 호출하면 내부에서 어떤 동작이 발생하는지 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    ...
}

  • put 메소드 호출 시 hash(key) 메소드를 먼저 호출한다.
  • hash(key) 메소드에서는 해당 key의 Object class에 있는 hashcode() method를 실행하고 리턴 값인 int를 얻어낸 후 16번 오른쪽으로 unsigned right shift한 값과 XOR을 하여 사용할 hash값을 얻는다.
  • putVal(...) 메소드에서는 해당 키의 해시코드, 키, 값 등등을 통해 node를 생성해 값을 저장한다.
    • 같은 키를 넣으면 원래 HashMap에 있던 키를 치환한다.
    • 이 때 하나의 버킷에 TREEIFY_THRESHOLD에 설정한 개수만큼 키/값 쌍이 모이면 버킷을 TreeNode로 바꾼다. (처음부터 바꾸지않는이유는 TreeNode는 리스트 노드보다 약 2배 커서 그만큼 공간을 더 차지하기 때문이다.)
    • 또한 threshold와 HashMap의 크기를 비교하여 재해시를 할 지 결정한다.

1-3 HashMap 충돌

  • 동일하지 않은 어떤 객체 X와 Y가 있을 때, 즉 X.equals(Y)가 ‘거짓’일 때 X.hashCode() != Y.hashCode()가 같지 않다면, 이때 사용하는 해시 함수는 완전한 해시 함수(perfect hash functions)라고 한다
  • HashMap은 기본적으로 각 객체의 hashCode() 메서드가 반환하는 값을 사용하는 데, 결과 자료형은 int다. 32비트 정수 자료형으로는 완전한 자료 해시 함수를 만들 수 없다.
  • 이는 해시 함수가 얼마나 해시 충돌을 회피하도록 잘 구현되었느냐에 상관없이 발생할 수 있는 또 다른 종류의 해시 충돌이다.
  • 이렇게 해시 충돌이 발생하더라도 키-값 쌍 데이터를 잘 저장하고 조회할 수 있게 하는 방식에는 대표적으로 두 가지가 있는데, 하나는 Open Addressing이고, 다른 하나는 Separate Chaining이다.

    해시 충돌 해결 방식

    • Open Addressing은 데이터를 삽입하려는 해시 버킷이 이미 사용 중인 경우 다른 해시 버킷에 해당 데이터를 삽입하는 방식이다.
    • Separate Chaining에서 각 배열의 인자는 인덱스가 같은 해시 버킷을 연결한 링크드 리스트의 첫 부분(head)이다. 이때 자바 8 이후부터는 데이터의 개수가 많아지면, Separate Chaining에서 링크드 리스트 대신 트리를 사용한다.
  • Java HashMap에서 사용하는 방식은 Separate Channing이다.

1-3-1 Java 8 HashMap에서의 Separate Chaining

1
2
3
4
5
6
7
8
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    ...
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        treeifyBin(tab, hash);
}
  • 링크드 리스트를 사용할 것인가 트리를 사용할 것인가에 대한 기준은 하나의 해시 버킷에 할당된 키-값 쌍의 개수이다.
  • Java 8 HashMap에서는 상수 형태로 기준을 정하고 있다.
  • 즉 하나의 해시 버킷에 8개의 키-값 쌍이 모이면 링크드 리스트를 트리로 변경한다. 만약 해당 버킷에 있는 데이터를 삭제하여 개수가 6개에 이르면 다시 링크드 리스트로 변경한다.

또한 Java 8 HashMap에서는 Entry 클래스 대신 Node 클래스를 사용한다.

Node 클래스 자체는 사실상 Java 7의 Entry 클래스와 내용이 같지만, 링크드 리스트 대신 트리를 사용할 수 있도록 하위 클래스인 TreeNode가 있다는 것이 Java 7 HashMap과 다르다.

이때 사용하는 트리는 Red-Black Tree이다. 트리 순회 시 사용하는 대소 판단 기준은 해시 함수 값이다. 해시 값을 대소 판단 기준으로 사용하면 Total Ordering에 문제가 생기는데, Java 8 HashMap에서는 이를 tieBreakOrder() 메서드로 해결한다

Red-Black Tree는 기본 이진 트리구조에 메타데이터를 부가(노드 컬러링)해서 트리 균형이 한쪽으로 치우치는 현상을 방지한 트리이다.

또한 put 메서드 구현 시 해시 버킷의 인덱스를 가져오는 것이 개선되었다.

1-3-2 해시 버킷 동적 확장

해시 버킷 개수의 기본값은 16이고, 데이터의 개수가 임계점에 이를 때마다 해시 버킷 개수의 크기를 두 배씩 증가시킨다. 버킷의 최대 개수는 230개다. 그런데 이렇게 버킷 개수가 두 배로 증가할 때마다, 모든 키-값 데이터를 읽어 새로운 Separate Chaining을 구성해야 하는 문제가 있다.

1
2
3
void resize(int newCapacity) {
    ...
}

1-3-2 보조해시함수

  • 보조 해시 함수(supplement hash function)의 목적은 ‘키’의 해시 값을 변형하여, 해시 충돌 가능성을 줄이는 것이다.
  • Java 8에서는 이전 자바 버전보다 훨씬 더 단순한 형태의 보조 해시 함수를 사용한다.
    • 상위 16비트 값을 XOR 연산하는 매우 단순한 형태의 보조 해시 함수를 사용한다.
1
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

2. LinkedHashMap

  • HaspMap의 서브 클래스로 이중 연결 리스트를 이용해 원소 삽입 순서를 관리한다.
  • LinkedHashMap의 기본 관리 모드는 삽입 순서이지만 액세스 순서 모드로 바꿀 수 있다.

2-1 LinkedHashMap 주요 필드

1
2
3
transient LinkedHashMap.Entry<K,V> head;
    ...
transient LinkedHashMap.Entry<K,V> tail;
  • doubly linked list 를 위한 head와 tail을 저장하는 변수가 선언되어있다.

2-2 LinkedHashMap put 메소드

  • LinkedHashMap은 HashMap의 put 메소드를 동일하게 사용하며 내부적으로 putVal이라는 메소드를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
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); //newNode가 override되어있다
    else {
      Node<K,V> e; K k;
      if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
	...
  • LinkedHashMap은 이 newNode를 override 하여, 새로운 노드를 추가할 때마다 LinkedHashMap이 관리하고 있는 head와 tail에 연결한다.

3. TreeMap

  • Red-Black Tree를 구현한 Map이다.
  • 다양한 키가 필요할 때 유용하며 서브 맵에 신속히 접근할 수 있다.

참고

  • https://www.baeldung.com/java-hashmap-load-factor
  • https://d2.naver.com/helloworld/831311
  • http://egloos.zum.com/iilii/v/4457500
  • https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html
This post is licensed under CC BY 4.0 by the author.