소스 코드 시리즈의 HashMap

소개

  HashMap은 Java 프로그래머가 가장 일반적으로 사용하는 컬렉션 중 하나일 수 있으며 인터뷰도 일반적인 시험 문제 중 하나입니다. 그렇다면 우리가 일반적으로 알고 있는 특징은 어떻게 나오는 것인지, 한동안은 연결리스트(Linked List)는 무엇이고, 잠시 동안은 레드-블랙 트리(Red-Black Tree)는 무엇인가. 고정 에세이 참가자들은 이미 고등학교 때 "등왕정 서문"을 외우고 근육 반응처럼 머리 속으로 낭독했는데 이것이 좋지 않다는 것이 아니라 그 이유를 알아야 합니다. 다음으로 JDK 1.8을 기반으로 한 HashMap의 소스코드를 공유하겠습니다. HashMap을 이제 막 배우기 시작한 학생들에게는 적합하지 않을 수 있으며, 특정 기초가 필요합니다. JDK 1.8의 HashMap에는 13개의 멤버 변수, 51개의 멤버 메소드 및 14개의 내부 클래스를 포함하여 총 2425줄의 코드가 있습니다. 다음으로 이에 대해 말씀드리겠습니다.

HashMap의 일부 기능

  소스 코드에 대해 공식적으로 이야기하기 전에 HashMap의 몇 가지 고정관념적인 기능을 검토해 보겠습니다. 이러한 기능이 포함된 소스 코드를 보면 다음과 같은 느낌을 가질 수 있습니다.

  • HashMap은 스레드 동기화를 지원하지 않으므로 스레드로부터 안전하지 않습니다.
  • HashMap의 키 값은 최대 하나의 null 값만 저장할 수 있으며, 값은 여러 개의 null 값을 저장할 수 있습니다.
  • HashMap은 순서가 없습니다.
  • 기본 초기 어레이 용량은 16입니다.
  • HashMap 배열의 최대 용량은 2의 N승입니다.
  • 사용자가 설정한 초기 용량 값에 따라 2의 N제곱이 가장 작은 값을 위쪽으로 검색합니다.
  • JDK 1.8에서는 레드-블랙 트리(red-black tree) 구조가 새로 추가되었는데, 연결 리스트의 길이가 8보다 크고, 배열의 길이가 64보다 큰 경우 링크드 리스트를 레드-블랙 트리로 변환하여 속도를 높인다. 연결리스트의 검색 성능은 O(n)이므로, 레드-블랙 트리의 검색 성능은 O(log(n))이다.
  • HashMap 용량이 어레이 최대 용량의 0.75배가 되면 확장을 하고, 용량은 2배로 증가한 후 원본 데이터를 새로운 어레이에 복사하게 되는데, 확장은 시간 낭비다. , 확장 횟수를 줄입니다!
  • 해시 충돌 처리 시 연결리스트 값 추가 시(레드-블랙 트리는 사용하지 않음) 1.7은 헤드 삽입 방식, 1.8은 테일 삽입 방식이다.
  • 스레드가 안전하지 않고 null을 허용하는 것을 제외하고 HashTable 사용법에는 차이가 없습니다.

HashMap 클래스 구조

소스 코드는 다음과 같습니다.

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
      ...  }

클래스 구조 다이어그램은 다음과 같습니다.
사진 설명을 추가해주세요
  HashMap이 AbstractMap을 상속했고 AbstractMap 클래스가 Map 인터페이스를 구현했는데 HashMap이 Map 인터페이스를 다시 구현하는 이유는 무엇입니까? 그리고 ArrayList의 LinkedList에도 유형이 있는데, 불필요한 것 아닌가요? 나중에 확인해보니 창업자 조쉬 블로흐(Josh Bloch)의 설명에 따르면 이런 글쓰기 방식은 실수였다는 것을 알게 되었습니다. Java 컬렉션 프레임워크에는 이렇게 작성하는 방법이 여러 가지 있는데, 처음 Java 컬렉션 프레임워크를 작성할 때 이런 방식으로 작성하는 것이 어떤 곳에서는 가치가 있을 것이라고 생각하다가 자신이 틀렸다는 것을 깨달았습니다. 분명히 JDK 관리자는 이 작은 실수를 수정할 가치가 없다고 생각하여 이렇게 저장했습니다. 사실 JDK에는 이렇듯 이력에 남는 문제가 많이 있는데, 일부는 호환성을 위한 것이고, 일부는 불필요하다고 판단되면 변경하지 않을 수도 있으니 소스코드를 두려워할 필요는 없다. 다 사람이 쓴거라 틀린 부분도 있을 수 있는데 큰 차이점이 뭔가요? , 한 번도 못 본다, 10 번 봐도 안 되나?
  여기서 우리는 Map 인터페이스를 다시 구현하는 것이 실수라는 것을 알고 있으며 괜찮습니다. AbstractMap은 주로 Map 형태의 데이터 구조의 공통 연산을 포함하고 있으며, Cloneable은 HashMap을 복제할 수 있다는 의미이고, Serialized는 HashMap을 직렬화할 수 있다는 의미입니다.

HashMap의 멤버 변수

  HashMap의 멤버변수에 대한 소개는 다음과 같습니다.멤버변수에서 바로 엿볼 수 있는 기능이 많습니다.

  • static final long serialVersionUID = 362498820763181265L,
    직렬화 중에 사용될 serialVersion 값입니다.
  • static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    기본 테이블 초기 용량, 즉 기본 초기 용량은 16(2의 N승이어야 함)입니다.
  • static final int MAXIMUM_CAPACITY = 1 << 30;
    테이블의 최대 용량, 즉 2의 30승입니다.
  • static final float DEFAULT_LOAD_FACTOR = 0.75f,
    용량 확장에 사용되는 부하 계수, 기본값은 0.75입니다.
  • static final int TREEIFY_THRESHOLD = 8;
    연결리스트의 길이가 임계값보다 크면 레드-블랙 트리로 변환됩니다.
  • static final int UNTREEIFY_THRESHOLD = 6;
    트리의 노드 수가 이 매개변수보다 작거나 같을 때 연결 목록으로 변환됩니다. 1.8에서는 연결 목록이 8개로 확장된 것을 우리 모두 알고 있습니다. 해시 충돌이 발생하면 레드-블랙 트리로 변환됩니다. 그러면 레드-블랙 트리를 아십니까? 6보다 작으면 연결 리스트로 퇴화됩니까?
  • static final int MIN_TREEIFY_CAPACITY = 64;
    컨테이너가 트리링할 수 있는 최소 테이블 용량, 즉 HashMap 배열이 64보다 크고, 단일 연결 리스트가 8보다 크고, 레드-블랙 트리가 변환됩니다. .
  • 임시 Node<K,V>[] 테이블,
    해시의 배열, 여기에서 배열에 저장된 내용이 실제로 Node 요소, 즉 전체 키와 값이며 크기는 다음을 기반으로 함을 알 수 있습니다. 초기화 시 사용자가 설정한 초기값 2의 정수배로 반올림합니다.
  • 일시적인 Set<Map.Entry<K,V>> EntrySet;
    캐시 EntrySet()을 저장합니다.
  • 일시적인 정수 크기,
    현재 HashMap에 포함된 키-값 쌍의 수를 나타냅니다.
  • 일시적인 int modCount;
    주로 반복의 빠른 실패에 사용되는 현재 HashMap 수정 수를 나타냅니다(이 값은 동시 수정이 수행되는지 여부와 동시 수정이 직접 예외를 발생시키는 경우를 결정하는 데 사용될 수 있음). 예를 들어 put()은 한 번 +1이 되지만 특정 키가 해당 값에 해당됩니다. 덮어쓰는 값은 구조적인 변화가 아닙니다.
  • int Threshold;
    현재 HashMap이 보유할 수 있는 키-값 쌍의 최대 수를 나타냅니다. 이 수를 초과하면 HashMap이 현재 최대 용량 * loadFactor와 동일하게 확장됩니다.
  • float loadFactor;
    해시 테이블의 로드 인자입니다. 부하율은 실제로 1보다 클 수 있습니다. 알고 계셨나요? 이렇게 큰 값을 설정하지 않는 이유는 해시 충돌이 너무 커서 쿼리 효율성에 심각한 영향을 미치기 때문입니다. 공간과 검색 시간의 균형을 맞추려면 0.75가 최선의 선택입니다.
    모든 멤버 변수는 아래 그림과 같습니다.
    사진 설명을 추가해주세요

HashMap의 멤버 메소드

   다음은 HashMap의 멤버 메소드들인데, 각 메소드에 대해 간략하게 설명하고, 중요한 메소드들에 대해 자세히 설명한다.

  • public HashMap(intinitialCapacity, float loadFactor);
    생성자, 초기 용량 및 부하율을 직접 설정할 수 있습니다. 여기서 부하율은 1보다 크거나 0.75보다 작을 수 있으며 메모리 및 응답 시간에 따라 선택할 수 있습니다.

    public HashMap(int initialCapacity, float loadFactor) {
          
          
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
  • public HashMap(intinitialCapacity);
    생성자, 초기 용량 값을 직접 설정할 수 있으며 부하 계수는 0.75입니다.

  • public HashMap();
    초기 용량이 16이고 로드 계수가 0.75인 기본 생성자입니다.

  • public HashMap(Map<? 확장 K, ? 확장 V> m);
    초기 용량이 16이고 로드 계수가 0.75인 생성자는 지정된 Map과 동일한 매핑 구조를 사용하여 새 HashMap을 구성합니다.

  • static final int hash(Object key);
    해시는 해시 값 + 섭동 함수입니다. 이는 먼저 키에 대한 hashCode 값을 가져온 다음 이를 방해한 다음 이를 해시 값으로 사용하여 높은 비트가 작업에 참여하고, 해시 충돌을 줄이고, 노드 값이 보다 균등하게 분산되도록 하며, 많은 함수가 해시(객체 키) 방법을 사용하여 해시 값을 계산합니다.

static final int hash(Object key) {
    
    
    int h;
    // h = key.hashCode() 为第一步 取hashCode值
    // h ^ (h >>> 16)  为第二步 低16位与高16位做异或运算,使得之后在做&运算时,此时的低位实际上是高位与低位的结合,可大大增加随机性,减少了碰撞冲突的可能性
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

예를 들어 n이 기본값 16일 때, tab[i = (n - 1) & hash]는 해시 값의 상위 비트 변화가 큰 경우 &를 사용하여 노드가 떨어져야 하는 위치를 방해 없이 판단합니다. , 낮은 비트의 변화가 작다면 많은 값이 같은 위치에 떨어지게 되고 충돌은 매우 커질 것입니다.

  • static Class<?> ComparableClassFor(Object x);
    ​ " Class C Implements Comparable " 형식인 경우 x의 클래스를 반환합니다.
  • static int CompareComparables(Class<?> kc, Object k, Object x);
    ​ x가 kc와 일치하면 k.compareto(x)를 반환하고, 그렇지 않으면 0을 반환합니다.
  • static final int tableSizeFor(int cap);
    ​ 주어진 목표 용량에 대해 위쪽으로 가져오는 최소 2의 N제곱을 반환합니다. 예를 들어 7이 주어지면 차지하면 8이 되고, 주어지면 8이 됩니다. 13을 취하면 16이 되며, 비트연산을 추론하여 계산한다.
	static final int tableSizeFor(int cap) {
    
    
		// 先减一,是为了避免原本就是2的N次幂
        int n = cap - 1;
    	// 或操作,只要有一个1,结果则为1
        // 以下五步位运算操作,相当于把最高位到最后一位的数字全变成1
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        // 如果最后,n不小于0,且不大于等于MAXIMUM_CAPACITY,则返回n + 1,n + 1则是给定目标容量向上取的最小2的N次幂
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
  • final void putMapEntries(Map<? 확장 K, ? 확장 V> m, boolean evict)는
    Map.putAll을 사용하여 public HashMap(Map<? 확장 K, ? 확장 V에 의해 생성된 수신 맵에 따라 새 맵을 생성합니다. > m) 함수의 실제 실행자입니다.
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    
    
    int s = m.size();
    if (s > 0) {
    
    
      	// 如果当前数组为空,初始化阈值
        if (table == null) {
    
     // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 如果不为空,且大于阈值,则扩容
        else if (s > threshold)
            resize();
      	// 扩容后,遍历增加所有值即可
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
    
    
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}
  • public int size();
    현재 해시 컬렉션의 길이를 반환합니다.
  • public boolean isEmpty()는
    현재 해시 컬렉션이 비어 있는지 여부를 반환합니다.
  • public V get(Object key);
    현재 컬렉션의 키에 해당하는 값을 반환합니다. 키가 존재하지 않으면 null을 반환합니다. null을 반환한다고 해서 반드시 키가 존재하지 않거나 값이 존재하지 않는다는 의미는 아닙니다. 비어 있을 수 있습니다.
  • final Node<K,V> getNode(int hash, Object key);
    get(Object key) 메소드의 실제 실행자는 주로 키로 계산된 해시 값을 통해 배열 내 위치를 파악한 후 트리 또는 객체에서 실행한다. 연결된 목록 찾기, 다음 코드는 소스 코드의 여러 위치에서 사용되므로 주의 깊게 읽어야 합니다.
// 传入key值算出的hash值,和key值
final Node<K,V> getNode(int hash, Object key) {
    
    
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 如果哈希数组不为空 且 长度大于0 且 hash值对应该的table[i]不为空,则进行查找,否则返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
    		// (n - 1) & hash 比单独取模快
        (first = tab[(n - 1) & hash]) != null) {
    
    
        // 如果对应table[i]第一个节点匹配,则直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果对应table[i]第一个节点不匹配,且next不为空,则继续匹配    
        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;
}
  • public boolean containKey(Object key)는
    주어진 키 값이 포함되는지 여부를 결정합니다. getNode(int hash, Object key)를 직접 호출하고 값이 null이 아니면 true를 반환합니다.

  • 값을 직접 추가하는 공개 V put(K 키, V 값) 연산은 putVal(…) 메서드를 호출하는데, 이는 아래에서 설명할 전체 HashMap의 핵심 중 하나라고 할 수 있습니다.
  • final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict);
    ​ put(K key, V value)의 구체적인 구현, 코드 분석은 다음과 같습니다.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    
    
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果tab没有初始化,则进行扩容初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果对应tab[i]为空,没有发生碰撞,则直接加入新节点    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 发生碰撞进行如下操作    
        else {
    
    
            Node<K,V> e; K k;
            // 如果第一个节点匹配,则覆盖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);
                        // 如果尾插后大于 TREEIFY_THRESHOLD ,则转化为红黑树
                        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;
            }
        }
        // 修改次数加1
        ++modCount;
        // size加1,如果超过threshold,则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

전체 과정은 아래 그림과 같습니다.
여기에 이미지 설명을 삽입하세요

  • final Node<K,V>[] resize()
    확장 resize()는 용량을 다시 계산하여 확장하는 것으로 HashMap 객체에 요소가 계속 추가되고 HashMap 객체 내부의 배열이 더 많은 요소를 로드할 수 없는 경우 객체는 더 많은 요소를 수용할 수 있도록 배열의 길이를 확장해야 합니다. 물론 Java에서 배열은 자동으로 확장될 수 없습니다. 방법은 새 배열을 생성하고 원래 값을 모두 새 배열에 "복사"하는 것입니다. 복사는 시간이 많이 걸리는 작업이므로 시도해 보아야 합니다. 초기화할 때 최선을 다합니다. 복사 동작을 줄이기 위해 배열 크기를 지정할 수 있습니다.

​ 코드는 다음과 같이 구문 분석됩니다.

		final Node<K,V>[] resize() {
    
    
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 如果不是第一次初始化,新容量为原来的两倍(<< 1)
        if (oldCap > 0) {
    
    
            if (oldCap >= MAXIMUM_CAPACITY) {
    
    
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {
    
                   // zero initial threshold signifies using defaults
            // 如果是第一次初始化,则用默认参数初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果newThr为0,则我们重新计算该值等于newCap * loadFactor;
        if (newThr == 0) {
    
    
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({
    
    "rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
    
    
        		// 把每个bucket都移动到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
    
    
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
    
    
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {
    
     // preserve order
                    		// 链表优化重hash的代码块
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
    
    
                            next = e.next;
                            // 原索引
                            if ((e.hash & oldCap) == 0) {
    
    
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引 + oldCap
                            // 1.8之后的扩容为原来的两部,无需重新计算hash值,可直接计算位置
                            else {
    
    
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket里
                        if (loTail != null) {
    
    
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
    
    
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • final void treeifyBin(Node<K,V>[] tab, int hash)
    테이블이 너무 작지 않은 한 주어진 해시 아래의 bin에 연결된 모든 노드를 대체합니다. 이 경우 대신 크기가 조정됩니다.
final void treeifyBin(Node<K,V>[] tab, int hash) {
    
    
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    
    
        TreeNode<K,V> hd = null, tl = null;
        do {
    
    
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
    
    
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
  • public void putAll(Map<? 확장 K, ? 확장 V> m);
    맵의 모든 값을 추가하고 putMapEntries(Map<? 확장 K, ? 확장 V> m, boolean evict) 메서드를 직접 호출합니다.
  • public V Remove(Object key);
    ​ 노드를 삭제하려면 RemoveNode() 메서드를 직접 호출하세요.
  • final Node<K,V> RemoveNode(int hash, Object key, Object value, boolean matchValue, boolean movable)
    getNode(int hash, Object key) 메소드와 유사한 노드 삭제 작업, 지정된 노드인 경우 먼저 찾습니다. 트리 구조라면 트리 형태의 삭제 연산을 사용하고, 연결 리스트 구조라면 연결 리스트 삭제 방식을 사용한다.
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    
    
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
    
    
        Node<K,V> node = null, e; K k; V v;
      	// 首先查找到该节点,和getNode(int hash, Object key)方法类似
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
    
    
          	// 如果是红黑树,则用树的查找方式,如果是链表,则直接遍历查找到为止
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
    
    
                do {
    
    
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
    
    
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
      	// 如果找到该节点,则进行删除操作,并返回该节点值
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
    
    
          	// 是红黑树,则用树的操作
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 链表则用链表的删除操作
          	else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
          	// 修改次数 + 1
            ++modCount;
          	// 尺寸 - 1
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
  • public voidclear()
    ​ 컬렉션의 모든 요소를 ​​삭제합니다.
	public void clear() {
    
    
        Node<K,V>[] tab;
        // 修改次数加1
        modCount++;
        if ((tab = table) != null && size > 0) {
    
    
            // 尺寸归0
            size = 0;
            // 释放所有链表或者红黑树,这些对象会在垃圾回收中被回收
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }
  • public boolean containValue(Object value);
    컬렉션 Value에 특정 값이 있는지 확인합니다.
  • public Set<K> keySet();
    모든 키 세트를 반환합니다.
  • public Collection<V> value()는
    모든 값의 컬렉션을 반환합니다.
  • public Set<Map.Entry<K,V>>entrySet()
    ​ 키와 해당 값을 포함하는 모든 항목의 컬렉션을 반환합니다.
  • public V getOrDefault(Object key, V defaultValue);
    ​ 키에 해당하는 값을 찾고, 존재하지 않으면 전달된 defaultValue인 기본값을 반환합니다.
  • public V putIfAbsent(K key, V value);
    값이 존재하지 않으면 저장하고 putVal(…)을 직접 호출합니다.
  • public boolean Remove(Object key, Object value);
    ​ 노드를 삭제하려면 RemoveNode(…)를 직접 호출하세요.
  • public boolean replacement(K key, V oldValue, V newValue);
    노드를 교체하기 위해 실제로 getNode(…)가 노드를 찾은 후 교체에 성공하면 true를 반환하고 실패하면 false를 반환합니다.
  • public V replacement(K key, V value)
    ​ 실제로 노드를 교체하려면 getNode(…)가 노드를 찾은 후 교체에 성공하면 해당 노드의 값을 반환하고, 실패하면 null을 반환합니다.
  • public V ComputeIfAbsent(K key, Function<? super K, ?extends V> mappingFunction);
    주어진 노드가 존재하지 않으면 mappingFunction을 사용하여 새 노드 값을 생성합니다.
  • public V ComputeIfPresent(K key, BiFunction<? super K, ? super V, ?extends V> remappingFunction)
    주어진 노드가 존재하는 경우 remappingFunction을 사용하여 새 값을 계산하고 생성합니다.
  • public V Compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
    주어진 노드 값과 remappingFunction으로 계산하여 새로운 값을 생성합니다. 주어진 노드 값이 존재하지 않는 경우 null을 사용합니다. 계산값 .
  • public V merge(K key, V value, BiFunction<? super V, ? super V, ?extends V> remappingFunction);
    remappingFunction 함수를 사용하여 키에 해당하는 값을 매개변수 값과 비교하고 병합합니다.
  • public void forEach(BiConsumer<? super K, ? super V> action)은
    컬렉션의 요소에 대해 작동하는 작업을 허용할 수 있는 순회 작업입니다.
  • public void replacementAll(BiFunction<? super K, ? super V, ? extends V> function)
    컬렉션의 요소에 대해 작동하는 함수를 허용할 수 있는 교체 작업입니다.
  • public Object clone();
    HashMap을 복제하고 반환
  	public Object clone() {
    
    
      HashMap<K,V> result;
      try {
    
    
          result = (HashMap<K,V>)super.clone();
      } catch (CloneNotSupportedException e) {
    
    
          // this shouldn't happen, since we are Cloneable
          throw new InternalError(e);
      }
      // 初始化一些信息
      result.reinitialize();
      // 加入当前集合所有元素
      result.putMapEntries(this, false);
      return result;
  }
  • final float loadFactor();
    ​ 로드 팩터 값을 반환합니다.
  • final int capacity()는
    현재 용량 값을 반환합니다.
  • private void writeObject(java.io.ObjectOutputStream s);
    ​ HashMap을 스트림에 저장하고 직렬화합니다.
  • private void readObject(ObjectInputStream s)는
    스트림에서 HashMap을 읽고 역직렬화합니다.
  • Node<K,V> newNode(int hash, K key, V value, Node<K,V> next)
    ​ 일반(비트리) 노드를 생성합니다.
  • Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next);
    ​ TreeNodes에서 일반 노드로 변환하는 데 사용됩니다.
  • TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) 는
    트리 노드를 생성합니다.
  • TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next);
    TreeNode에서 일반 노드로 변환하는 데 사용됩니다.
  • void reinitialize()
    ​ 초기 기본 상태로 재설정합니다. clone 및 readObject에 의해 호출됩니다.
  • void afterNodeAccess(Node<K,V> p);
    LinkedHashMap 사후 작업 콜백을 허용합니다.
  • void afterNodeInsertion(boolean evict)
    ​ LinkedHashMap 사후 작업 콜백을 허용합니다.
  • void afterNodeRemoval(Node<K,V> p);
    LinkedHashMap 사후 작업 콜백을 허용합니다.
  • void InternalWriteEntries(java.io.ObjectOutputStream s);
    ​ 호환 가능한 순서를 보장하기 위해 writeObject에서만 호출됩니다.
    모든 방법은 아래 그림에 나와 있습니다.
    사진 설명을 추가해주세요
    사진 설명을 추가해주세요

HashMap의 내부 클래스

  다음은 HashMap의 내부 클래스입니다.

  • static class Node<K,V> Implements Map.Entry<K,V>{…}
    단일 노드의 값이 존재하는 가장 기본적인 HashMap 노드입니다.

    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;
            if (o instanceof Map.Entry) {
          
          
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
    
  • final 클래스 KeySet은 AbstractSet<K>{…}를 확장하고
    AbstractSet 추상 클래스를 상속하며 컬렉션에 키 값을 저장하는 데 사용됩니다.

  • final 클래스 Values ​​​​extends AbstractCollection<V>{…}는
    컬렉션에 값을 저장하는 데 사용되는 AbstractSet 추상 클래스를 상속합니다.

  • 최종 클래스 EntrySet은 AbstractSet<Map.Entry<K,V>>{…}를 확장하며
    AbstractSet 추상 클래스를 상속하고 컬렉션에 항목 컬렉션을 저장하는 데 사용됩니다.

  • private static final class UnsafeHolder{…}는
    역직렬화 중에 최종 필드 재설정을 지원합니다.

  • abstract class HashIterator{…}
    HashMap의 일반 반복자입니다.

    abstract class HashIterator {
          
          
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
    
        HashIterator() {
          
          
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) {
          
           // advance to first entry
                do {
          
          } while (index < t.length && (next = t[index++]) == null);
            }
        }
    
        public final boolean hasNext() {
          
          
            return next != null;
        }
    
        final Node<K,V> nextNode() {
          
          
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
          
          
                do {
          
          } while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    
        public final void remove() {
          
          
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }
    
  • 최종 클래스 KeyIterator 확장 HashIterator 구현 Iterator<K> {…}
    키에 대한 반복자인 HashIterator에서 상속됩니다.

  • 최종 클래스 ValueIterator 확장 HashIterator 구현 Iterator<V>{…}
    값에 대한 반복자인 HashIterator에서 상속됩니다.

  • 최종 클래스 EntryIterator 확장 HashIterator 구현 Iterator<Map.Entry<K,V>>{…}
    HashIterator에서 상속됨, 항목용 반복자.

  • static class HashMapSpliterator<K,V>{…}
    요소를 병렬로 탐색하기 위해 jdk 1.8에 추가된 새로운 함수인 HashMap의 범용 분할 반복자입니다.

    static class HashMapSpliterator<K,V> {
          
          
        final HashMap<K,V> map;
        Node<K,V> current;          // current node
        int index;                  // current index, modified on advance/split
        int fence;                  // one past last index
        int est;                    // size estimate
        int expectedModCount;       // for comodification checks
    
        HashMapSpliterator(HashMap<K,V> m, int origin,
                           int fence, int est,
                           int expectedModCount) {
          
          
            this.map = m;
            this.index = origin;
            this.fence = fence;
            this.est = est;
            this.expectedModCount = expectedModCount;
        }
    
        final int getFence() {
          
           // initialize fence and size on first use
            int hi;
            if ((hi = fence) < 0) {
          
          
                HashMap<K,V> m = map;
                est = m.size;
                expectedModCount = m.modCount;
                Node<K,V>[] tab = m.table;
                hi = fence = (tab == null) ? 0 : tab.length;
            }
            return hi;
        }
    
        public final long estimateSize() {
          
          
            getFence(); // force init
            return (long) est;
        }
    }
    
  • 정적 최종 클래스 KeySpliterator<K,V> 확장 HashMapSpliterator<K,V> 구현 Spliterator<K>{…}
    HashMapSpliterator에서 상속되며 키에 대한 분할 가능 반복자입니다.

  • static final class ValueSpliterator<K,V> 확장 HashMapSpliterator<K,V> 구현 Spliterator<V>{…}
    HashMapSpliterator에서 상속되며 값의 분할 가능 반복자에 사용됩니다.

  • 정적 최종 클래스 EntrySpliterator<K,V> 확장 HashMapSpliterator<K,V> 구현 Spliterator<V>{…}는
    HashMapSpliterator에서 상속되며 항목에 대한 분할 반복자로 사용됩니다.

  • static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>{…}
    레드-블랙 트리의 트리 구조는 상대적으로 조작이 복잡하므로 나중에 레드-블랙 트리에 대한 글을 작성하겠습니다. 당분간 논의되지 않을 것입니다.

    		static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
          
          
    				// 父节点
            TreeNode<K,V> parent;  // red-black tree links
            // 左子树
            TreeNode<K,V> left;
            // 右子树
            TreeNode<K,V> right;
            // 前一节点
            TreeNode<K,V> prev;    // needed to unlink next upon deletion
            // 是否为红色节点
            boolean red;
            
            TreeNode(int hash, K key, V val, Node<K,V> next) {
          
          
                super(hash, key, val, next);
            }
    
            // 返回根节点
            final TreeNode<K,V> root() {
          
          
                ...
            }
    
            // 确保给定的根节点是其bin的第一个节点
            static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
          
          
                ...
            }
    
            // 查找节点
            final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
          
          
                ...
            }
    
            // 调用根查找节点
            final TreeNode<K,V> getTreeNode(int h, Object k) {
          
          
                ...
            }
    
            
            static int tieBreakOrder(Object a, Object b) {
          
          
                ...
            }
    
            // 形成从该节点链接的节点树
            final void treeify(Node<K,V>[] tab) {
          
          
                ...
            }
    
            // 把树节点转化成链表节点
            final Node<K,V> untreeify(HashMap<K,V> map) {
          
          
                ...
            }
    
            // 向树中加入新值
            final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                           int h, K k, V v) {
          
          
                ...
            }
    
            // 从树中删除值
            final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                      boolean movable) {
          
          
                ...
            }
    
            // 将树分裂成两颗树
            final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
          
          
                ...
            }
    
            /* ------------------------------------------------------------ */
            // Red-black tree methods, all adapted from CLR
    				
    				// 左旋
            static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                                  TreeNode<K,V> p) {
          
          
                ...
            }
    
    				// 右旋
            static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                                   TreeNode<K,V> p) {
          
          
                ...
            }
    
            static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                        TreeNode<K,V> x) {
          
          
                ...
            }
    
            static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
                                                       TreeNode<K,V> x) {
          
          
                ...
            }
    
            // 递归检查不变量
            static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
          
          
                ...
            }
        }
    

모든 내부 클래스는 아래 그림과 같습니다.
사진 설명을 추가해주세요

HashMap의 다양한 메커니즘 구현

  지금 설명한 방법 소개 외에도 강조해야 할 몇 가지 중요한 메커니즘이 있다고 생각합니다. 자세한 내용은 아래를 참조하세요.

용량 초기화

   초기화는 사용자가 설정한 초기 용량 값을 기준으로 2의 거듭제곱이 가장 작은 값을 조회하게 되며, 자세한 내용은 위의 tableSizeFor(int cap) 메소드 설명을 참고하시기 바랍니다. 용량을 2의 N승으로 취하는 이유는 주로 이후의 모듈로 계산의 편의를 위한 것입니다.

해시 섭동 함수 방법

  Perturbation 함수의 주요 목적은 원래의 해시값을 보다 랜덤하게 만들어서 노드가 배열에 보다 고르게 분포될 수 있도록 하는 것인데, 이미 해시(객체 키) 방식에서 명확하게 설명했으므로 생략하겠습니다. 자세한 내용은 여기에서 확인하세요 .

해시 버킷 인덱스 위치 찾기

   HashMap에서 해시 버킷 인덱스 위치를 찾으려면 총 세 단계가 있습니다.

  • 키의 hashCode 값을 가져옵니다.
  • 낮은 비트와 높은 비트의 XOR 연산
  • 모듈로 연산

  처음 두 단계는 해시(객체 키)로 구현되었으며 프로세스도 명확하게 설명되어 있습니다. 여기서 초점은 모듈로 연산에 있습니다. 그러나 직접 모듈로 연산의 소비가 비교적 큽니다. 매우 영리한 방법이 사용됩니다. HashMap, 지금 당장:

h & (table.length -1)	

   HashMap의 기본 배열 길이는 항상 HashMap의 속도 최적화인 2의 n승입니다. 길이가 항상 2의 n승인 경우 h&(길이-1) 연산은 모듈로 길이, 즉 h%길이와 동일하지만 &가 %보다 더 효율적입니다. 여기서는 모듈러스를 취하여 인덱스 i를 계산한 후 table[i]를 직접 이용하여 해시값에 해당하는 위치를 찾는다.

확장

  확장이란 용량을 다시 계산하여 확장하는 것으로, HashMap 객체에 요소가 계속 추가되어 HashMap 객체 내부의 배열이 더 많은 요소를 담을 수 없는 경우, 더 많은 요소를 로드할 수 있도록 개체가 배열의 길이를 확장해야 합니다. 물론 Java에서 배열은 자동으로 확장될 수 없습니다. 방법은 새 배열을 생성하고 원래 값을 모두 새 배열에 "복사"하는 것입니다. 복사는 시간이 많이 걸리는 작업이므로 시도해 보아야 합니다. 초기화시 최선을 다하겠습니다. 배열의 크기를 지정하여 복사 동작을 줄이는 것이 가능합니다. 자세한 내용은 resize() 세부 설명을 참조하세요.

스레드가 안전하지 않음

  JDK 1.7에서는 멀티 스레드 작업으로 인해 무한 루프 문제가 발생할 수 있지만 JDK 1.8에서는 그렇지 않지만 여전히 스레드에 안전하지 않은 작업이므로 사용하지 않는 것이 좋습니다. ConcurrentHashMap은 멀티 스레드 동시성에 사용할 수 있습니다. ConcurrentHashMap의 소스코드 분석을 기대해달라는 글을 쓰겠습니다.

jdk 버전 1.7과 jdk 버전 1.8의 차이점

  • 1.7 기본 데이터 구조는 배열 + 연결 목록입니다. 1.8 기본 데이터 구조는 배열 + 연결 목록 + 레드-블랙 트리 구조입니다(연결 목록의 길이가 8보다 크고 배열이 64보다 크면 레드-블랙 트리로 변환됩니다).
  • 1.7에서 새로 추가된 노드는 헤드 삽입 방식이고, 1.8에서 새로 추가된 노드는 테일 삽입 방식인데, 테일 삽입 방식은 1.8에서 링 리스트가 쉽게 나오지 않는 이유이기도 합니다.
  • 1.7 교란 처리가 9회 수행됨 = 4비트 연산 + 5개의 XOR 연산, 1.8 교란 처리가 2회 수행됨 = 1비트 연산 + 1XOR 연산.
  • 1.7은 데이터를 삽입하기 전의 확장이고, 1.8은 데이터를 성공적으로 삽입한 후의 확장입니다.
  • 1.7 확장 후 위치 계산은 원본과 일치합니다. 즉, hashCode -> 교란 -> 모듈로입니다. 1.8 보다 효율적이기 위해 용량 확장 후의 법칙에 따라 계산됩니다: 용량 확장 후 위치 = 원래 위치 또는 원래 위치 + 이전 용량.

HashTable과의 차이점

  • 반면, HashTable은 스레드로부터 안전하며 키와 값이 null이 되는 것을 허용하지 않습니다.
  • HashTable의 기본 용량은 11입니다.
  • HashTable은 정적 최종 int 해시(객체 키) 섭동 함수를 사용하여 키의 hashCode를 해시 값으로 교란하는 HashMap과 달리 키의 hashCode(key.hashCode())를 직접 해시 값으로 사용합니다.
  • HashTable은 모듈로 연산 %를 사용하여 직접 해시 버킷의 첨자를 취하며(기본 용량은 2의 n승이 아니기 때문에 모듈로 연산을 비트 연산으로 대체하는 것은 불가능합니다), HashMap에서 사용하는 비트 연산은 다음과 같습니다. 단순한 모듈로 연산보다 빠르며, 기본적으로 차원 축소 타격이 많습니다.
  • 확장 시 새로운 용량은 원래 용량의 2배 +1이 됩니다. int newCapacity = (oldCapacity << 1) + 1 .
  • Hashtable은 Dictionary의 하위 클래스이자 Map 인터페이스를 구현하며, HashMap은 Map 인터페이스의 구현 클래스입니다.
    HashTable은 스레드로부터 안전하지만 현재는 성능이 좋지 않아 기본적으로 사용되지 않으며 스레드로부터 안전한 작업이 필요한 경우 ConcurrentHashMap을 사용할 수 있습니다.

참고자료

Java 8 시리즈의 HashMap 재이해

추천

출처blog.csdn.net/qq_22136439/article/details/128434837