HashMap 소스 코드의 우아한 디자인에 대해 이야기하십시오.

1. HashMap 생성자
HashMap은 HashMap 객체를 생성하기위한 세 가지 생성자를 제공합니다.
1. 매개 변수없는 생성자 public HashMap () : 매개 변수없는 생성자에 의해 생성 된 해시 맵 객체의 기본 용량은 16이고 기본로드 계수는 0.75입니다.
2. 매개 변수 생성자 public HashMap (int initialCapacity, float loadFactor) :이 생성자를 사용하여 해시 맵의 초기 용량과로드 팩터를 지정할 수 있지만 해시 맵의 맨 아래에서 전달한 용량으로 반드시 초기화되지는 않습니다. 그러나 전달 된 값보다 크거나 같은 2의 최소 제곱으로 초기화됩니다. 예를 들어 17을 전달하면 해시 맵이 32 (2 ^ 5)로 초기화됩니다. 그렇다면 hashmap은 숫자보다 크거나 같은 2의 최소 거듭 제곱을 어떻게 효율적으로 계산합니까? 소스 코드는 다음과 같습니다.

static final int tableSizeFor(int cap) {
    
    
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  }

그 디자인은 매우 영리하다고 말할 수 있습니다. 기본 아이디어는 이진수의 하위 비트가 모두 1이면이 숫자 +1은 2의 거듭 제곱이어야한다는 것 입니다. 예를 들어 보겠습니다.
여기에 사진 설명 삽입

보시다시피, 계산 프로세스는 다음과 같습니다. 먼저 지정한 숫자 상한에서 1을 뺀 다음 (1을 빼는 이유는 상한이 정확히 2의 거듭 제곱 인 경우 올바르게 계산 될 수도 있기 때문입니다) 그런 다음 cap-1 Unsigned는 1, 2, 4, 8, 16 비트 (정확히 31 비트) 씩 오른쪽 시프트하고, 시프트 후 이전 숫자로 비트 OR 연산을 수행합니다.이 연산을 통해 최종 결과 하위 비트가 모두 1이되도록합니다. 그런 다음 마지막으로 결과에 1을 더하면 2의 거듭 제곱을 얻을 수 있습니다.
3. 매개 변수화 된 또 다른 생성자는 매개 변수화 된 생성자 public HashMap (int initialCapacity)이 생성자와 이전 생성자의 유일한 차이점은로드 계수를 지정할 수 없다는 것입니다.

2. HashMap 삽입 메커니즘
1. 삽입 메소드 소스 코드

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 初始化桶数组 table, table 被延迟到插入新数据时再进行初始化
        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;
            //如果hash相等,并且equals方法返回true,这说明key相同,此时直接替换value即可,并且返回原值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
             //如果第一个节点是树节点,则调用putTreeVal方法,将当前值放入红黑树中
            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);
                        //判断链表中节点树是否超多了阈值8,如果超过了则将链表转换为红黑树(当然不一定会转换,treeifyBin方法中还有判断)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果在链表中找到,完全相同的key,则直接替换value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e!=null说明只是遍历到中间就break了,该种情况就是在链表中找到了完全相等的key,该if块中就是对value的替换操作
            if (e != null) {
    
     // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //加入value之后,更新size,如果超过阈值,则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

2. 순서도 삽입
여기에 사진 설명 삽입
(1) kv를 넣을 때 먼저 hash () 메서드를 호출하여 키의 해시 코드를 계산하지만, 해시 맵에서는 단순히 해시 코드를 얻기 위해 키의 해시 코드를 호출하는 것이 아닙니다. 방해 기능은 해시 충돌을 줄이기 위해 사용됩니다. 소스 코드는 다음과 같습니다.

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

소스 코드에서 볼 수 있듯이 최종 해시 값은 원래 해시 코드와 원래 해시 코드를 오른쪽으로 16 비트 이동하여 얻은 값에 대한 XOR 연산의 결과입니다. 16은 정확히 32의 절반이므로 해시 맵은 해시 코드의 상위 비트를 하위 비트로 이동 한 다음 배타적 OR 연산을 통해 상위 비트를 하위 비트로 확산하여 해시 충돌을 줄입니다. 충돌을 줄일 수있는 이유에 대해서는 해시 메서드에 대한 작성자의 의견을 볼 수 있습니다.

key.hashCode ()를 계산하고 해시의 상위 비트를 하위로 확산 (XOR)합니다. 테이블은 2의 거듭 제곱 마스킹을 사용하기 때문에 현재 마스크 위의 비트 만 변하는 해시 세트는 항상 충돌합니다. (알려진 예 중에는 작은 테이블에 연속 된 정수를 포함하는 Float 키 세트가 있습니다.) 따라서 우리는 더 높은 비트의 영향을 아래쪽으로 확산시키는 변환을 적용합니다. 비트 확산의 속도, 유틸리티 및 품질 간에는 절충안이 있습니다. 많은 공통 해시 세트가 이미 합리적으로 분산되어 있기 때문에 (확산의 이점이 없음), 빈에서 큰 충돌 세트를 처리하기 위해 트리를 사용하기 때문에 시스템 손실을 줄이기 위해 가능한 가장 저렴한 방법으로 일부 이동 된 비트를 XOR합니다. 뿐만 아니라 테이블 경계로 인해 인덱스 계산에 사용되지 않는 가장 높은 비트의 영향을 통합합니다.

주석에서 저자가 높음에서 낮음으로 전파 한 이유는 다음과 같습니다. hashmap이 버킷 첨자를 계산할 때 계산 방법은 hash & n-1, n은 2의 거듭 제곱이므로 hash & n-1은 그냥 제거됩니다. 예를 들어, n이 16이면 hash & n-1은 해시의 하위 4 비트를 가져옵니다. 여러 해시의 하위 4 비트가 정확히 동일하면 해시가 다음과 같은 경우에도 항상 충돌 (충돌)이 발생합니다. 다른. 따라서 상위 비트는 하위 비트로 확산되고 상위 비트도 계산에 포함되어 충돌을 줄이고 데이터 저장을 더 많이 해시합니다.

(2) 해시를 계산 한 후 putVal 메서드를 호출하여 키-값을 저장합니다. putVal 메서드에서 먼저 테이블이 초기화되었는지 여부를 확인해야합니다 (해시 맵이 느리게 초기화되고 객체가 생성 될 때 테이블이 초기화되지 않기 때문). 테이블이 초기화되지 않은 경우 resize 메서드를 사용하여 넓히다.

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

(3) (n-1) & hash를 통해 현재 키가있는 버킷 첨자를 계산합니다. 현재 테이블의 현재 첨자에 저장된 데이터가없는 경우 링크드리스트 노드를 생성하고 현재 kv를 첨자에 직접 저장합니다. 위치.

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

(4) 테이블 아래 첨자에 이미 데이터가있는 경우 먼저 현재 키가 아래 첨자에 저장된 키와 정확히 같은지 확인합니다. 같으면 값을 직접 바꾸고 원래 값을 반환하고 그렇지 않으면 계속 탐색합니다. 연결 목록 또는 빨간색 검은 색 트리에 저장합니다.

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
      e = p;

(5) 현재 아래 첨자의 노드가 트리 노드 인 경우 레드-블랙 트리에 직접 저장됩니다.

else if (p instanceof TreeNode)
         e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

(6) Red-Black 트리가 아닌 경우 링크드리스트를 순회하고, 링크드리스트 순회 과정에서 등가 키가 발견되면 값을 교체하고, 등가 키가 없으면 노드를 연결 목록의 끝 (jdk8에서는 꼬리가 사용됨) 보간) 현재 연결 목록의 노드 트리가 임계 값 8을 초과하는지 확인합니다. 8을 초과하면 연결 목록이 다음과 같이 레드-블랙 트리로 변환됩니다. treeifyBin 메서드를 호출합니다.

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

(7) 데이터 저장 후 현재 해시 맵의 크기가 확장 임계 값 Cap * load_fact를 초과하는지 확인해야하며, 임계 값보다 크면 resize () 메서드를 호출하여 확장합니다.

f (++size > threshold)
       resize();

확장 후 HashMap의 용량은 원래 용량의 두 배입니다. 기본 메커니즘은 용량이 두 배인 테이블을 만든 다음 데이터를 새 해시 테이블에 덤프하고 새 해시 테이블을 반환하는 것입니다. jdk1.7과 다른 점은 jdk1.8의 멀티 덤핑이 최적화되었으며 버킷 첨자를 다시 계산할 필요가 없다는 것입니다. 구현의 소스 코드는 다음과 같습니다.
여기에 사진 설명 삽입
소스 코드에서 키 해시와 원래 용량 oldCap의 비트 AND 연산 결과가 0이면 확장 전의 버킷 첨자가 확장 후의 버킷 첨자와 같고, 그렇지 않으면 확장 후의 버킷 첨자가 원래 첨자와 oldCap입니다. 사용 된 기본 원칙은 다음과 같이 요약됩니다.

1. 숫자 m과 2의 거듭 제곱 n의 비트 AND 연산이 0이 아니면 : m & (n 2-1) = m & (n-1) + n
이해 : 2의 거듭 제곱 제곱수 n, 바이너리의 한 비트 만 1 (k 번째 비트가 1이라고 가정)하고 다른 비트는 모두 0입니다. 숫자 m과 n의 비트 AND 연산 결과가 0이면 이는 다음을 의미합니다. m의 이진수 k 비트는 0이어야하므로 m의 처음 n 비트와 처음 n-1 비트가 나타내는 값은 동일해야합니다.
2. 숫자 m과 2의 숫자 n의 거듭 제곱이 0이되도록 비트 AND 연산을 받으면 m & (n
2-1) = m & (n-1)
이해 : 2의 숫자 n의 거듭 제곱 , 바이너리 시스템에서는 1 비트 만 1 (k 번째 비트가 1이라고 가정)하고 나머지 비트는 모두 0입니다. 숫자 m과 n의 비트 AND 연산 결과가 0이 아니면 m의 이진법에서 k 번째 비트는 1이어야합니다. m의 처음 n 비트와 처음 n-1 비트가 나타내는 값의 차이는 정확히 k 번째 위치에서 1이 나타내는 숫자입니다. 두 번째 숫자는 정확히 n입니다.

개략도:
여기에 사진 설명 삽입

추천

출처blog.csdn.net/qq_40400960/article/details/114048299