ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • HashMap 내용물 이해하기
    ETC 2022. 9. 10. 00:47
    test

    Map

    Map이라고 하는 것은 Key 와 Value를 연결하는 자료구조로 특징적인 부분은

     

    Key와 Value 중 하나만 존재하지 않고 Key는 중복되지 않지만 Value는 중복될 수 있다는 특징이 있다.

    Java에서 Map은 인터페이스이며 내부적으로 구현이 되어있는 것은 아니다.

    구현체로 가장 보편적인 것은 HashMap이다.

    public interface Map<K, V> {
        
        int size();
        boolean isEmpty();
        boolean containsKey(Object key);
        boolean containsValue(Object value);
        V get(Object key);
    	...
    }
     

    HashMap

    public static void main(String[] args){
    	HashMap<String,String> map = new HashMap<>();
    }
     

    HashMap는 Key와 Hash 함수를 이용하여 index로 사용하고

    이 index에 해당하는 배열 위치에 value를 넣는 자료형이다.

     

    HashMap<String,String> map = new HashMap<>();
    map.put("key1","value1");
     

    위와 같이 HashMap에 저장한다고 해서

    전체를 탐색해서 A를 찾고 그에 맞는 Value를 찾는 것이 아닌

     

    'key1'를 Hash 함수를 통해 특정한 인덱스, 예로 3이 나왔다면

    배열 3번 방에 위치한 값을 꺼내보면 'value1' 가 들어있는 것이다.

     

     

    저장하는 로직부터 한번 찾아가 보자.

    public V put(K key, V value) {
    	return putVal(hash(key), key, value, false, true);
    }
     

    put 메서드를 호출하게 되면 내부적으로 Key를 hash 메서드에 넣고

    putVal 메서드를 호출하여 결과를 반환한다.

     

     

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

    key가 null이라면 0을 반환하고 아니라면

    key의 hashCode 값과 해당 값을 시프트 16한 값을 XOR 연산하여 해시값을 만든다.

     

    즉 key1이라는 값을 넘겨주면

    1100100010110110110010 ^ 110010 = 1100100010110110000000
     

    위와 같은 처리가 이루어진다.

     

    이제 이 값을 이용하여 putVal 메서드를 실행시킨다.

    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;
    }
     

    어...

    if ((tab = table) == null || (n = tab.length) == 0)
    		n = (tab = resize()).length;
     
    transient Node<K,V>[] table;
     

    멤버 변수 table를 변수 tab에 넣으면서 값이 null 이거나 길이가 0 인지 확인한다.

     

    값을 넣은 적이 있다면 null 이 아니겠지만 지금 처음 넣는 것이기 때문에 이 로직이 동작할 것이다.

     

    먼저 tab에 링크 리스트 초기화가 진행되고

    if ((p = tab[i = (n - 1) & hash]) == null)
    		tab[i] = newNode(hash, key, value, null);
     

    해당 인덱스에 아무 값이 없다면 새로운 노드를 넣게 된다.

     

    만약 해당 인덱스에 값이 있다면

    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 (p.hash == hash &&
    			((k = p.key) == key || (key != null && key.equals(k))))
    			e = p;
     

    노드를 교체한다.

     

     

    이번엔 값을 가져오는 로직을 살펴보자

    public V get(Object key) {
    	Node<K,V> e;
    	return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
     

    key를 해서 하여 getNode 메서드를 통해 값을 가져온다.

    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;
    }
     
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) 
     

    먼저 table 자체가 값이 존재하는지를 검사하는 로직이다.

    이 로직을 통과 못하면 해당 HashMap에는 아무 값이 없다는 것이다.

     

    if (first.hash == hash && // always check first node
    			((k = first.key) == key || (key != null && key.equals(k))))
    			return first;
     

    해시에 해당하는 노드를 꺼내서 해당 해시가 같은지 먼저 비교하고

    key가 같은지 비교하여 같다면 해당 노드를 꺼내서 반환한다.

     

    결국 나의 의문점은

    해시를 이용하여 해당 노드를 찾는 것으로 결국 저장한 value를 꺼낼 때

    해시 충돌이 나면 이것을 링크드 리스트로 연결한다는 것까지는 알고 있었으나

    링크드 리스트에서 어떻게 원하는 값을 찾는가 가 궁금했었는데

    static class Node<K,V> implements Map.Entry<K,V> {
    	final int hash;
    	final K key;
    	V value;
    	Node<K,V> next;
    ...
     

    노드 자체에 hash 와 key가 둘 다 있어서

    hash를 통해 해당 링크드 리스트를 찾아오고

    여기서 key가 같은 것을 찾아온다는 것을 알게 되었다.

     

     

    'ETC' 카테고리의 다른 글

    ?? : DB 의 인덱스 가 뭔가요?  (0) 2022.11.16
    그놈의 REST 한 API  (0) 2022.09.27
    RVA to RAW 쉽게 계산하기  (0) 2022.09.04
    AWS 서버 아키텍처 구축하기  (0) 2022.09.03
    Ubuntu 를 Jupyter Notebook 로 접속하기  (0) 2020.12.13