HashMap 原理

成员变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 数组默认长度

static final int MAXIMUM_CAPACITY = 1 << 30; // 数组长度最大值

static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子,数组长度达到 75% 就扩容

// TREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY 同时达到才会转红黑树
static final int TREEIFY_THRESHOLD = 8; // 添加元素,数组里的链表 >= 8 转为红黑树

static final int MIN_TREEIFY_CAPACITY = 64; // 添加元素,数组长度 >= 64 时,转为红黑树

static final int UNTREEIFY_THRESHOLD = 6; // 删除元素,数组里的链表默认 <= 6 时,如果是红黑树转为链表

transient Node<K,V>[] table; // 存储数据的数组

transient Set<Map.Entry<K,V>> entrySet; // 数据的集合

transient int size; // 元素个数

transient int modCount; // 修改次数

int threshold; // 扩容临界值,第一次 put 时初始化为 12,后面每次扩容维护一次

final float loadFactor; // 自定义的加载因子(如果创建实例时,指定了加载因子,默认的就不生效)

put 过程

假设是第一次 put ,只是看 put 过程,先不考虑 hash 碰撞、红黑树那些

// 入口方法
public V put(K key, V value) {
    // 先得到 key 的 hash 值
    return putVal(hash(key), key, value, false, true);
}

// 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) // 第一次进来 table 数组是空,
        n = (tab = resize()).length; // resize() 创建了一个长度是 16 的数组,扩容阈值是 12(虽然里面比较复杂,但是只看第一次添加,代码很简单就不贴了)
    // 此时数组下标是否为空,如果为空,床架一个节点,放入数组此下标
    if ((p = tab[i = (n - 1) & hash]) == null) // n 数组长度,再跟 key 的 hash 做与运算,这是在算 key 的下标
        tab[i] = newNode(hash, key, value, null);
    else {
		... 因为看第一次 put,这个 else 是不会走的
    }
    ++modCount; // 修改次数+1
    if (++size > threshold) // 维护 size,并判断是否需要扩容,达到扩容阈值没
        resize(); // 第一次添加,不会扩容
    afterNodeInsertion(evict); // LinkedHashMap 实现,跟 ArrayList 没关系
    return null;
}

hash 冲突

如果第二次添加,先假存在 hash 冲突,但是值不一样,因为如果值一样直接就覆盖完事儿了

// 还是 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) // 哈希冲突,这时 key 算出来的下标在数组中就有值了,不会进这个判断
        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)))) // 先 key 的 hash 值,再用 equals 判断 value
            e = p; // 如果相等,直接覆盖
        else if (p instanceof TreeNode) // 已经存在的元素如果是树节点(已经是红黑树了)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 添加到树里里面去
        else { // 如果不是树节点,那就是链表
            for (int binCount = 0; ; ++binCount) {
                // p 是已存在的节点,p 的下一个节点如果为空
                if ((e = p.next) == null) { // 尾插法,8 之前是头插法(七上八下)
                    p.next = newNode(hash, key, value, null); // 把这次 put 的数据封装成节点,并作为 p 的下一个节点(HashMap 是单向链表)
                    // 8-1=7,下标是7,第八个元素,如果已经遍历到这里,那就说明可能要转成红黑色了(如果数组长度也达到64才会真正转)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash); // 这个方法里面会判断数组长度是否达到 64,如果达到了,数据结构就转为红黑树
                    break; // 到这里就结束了,如果不用转红黑树,单向链表已经维护了,如果要转红黑树,也转了
                }
                // 如果 p 的下一个节点不为空才会这里,这里判断下一个节点,是否需要覆盖
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 如果需要覆盖,直接 break?因为这个循环的第一个判断就把该节点指给了 e,所以 e 不为空,跳出后就执行覆盖逻辑了
                p = e; // 如果不需要覆盖,再把 e 指给 p 这个变量,然后进入链表的下一个节点遍历
            }
        }
        // 如果 e 不为空,就是 hashCode 和 equals 都相同,要覆盖值
        if (e != null) { // existing mapping for key
            V oldValue = e.value; // e 原来的数据
            // 原来的数据为空或onlyIfAbsent为false,就进这个判断,判断也很简单,就是把新的 value 设置为节点的值
            if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent 传参进来的写死的是 false
                e.value = value; // put 带过来的 value 设置到节点的 value,完成值的覆盖
            afterNodeAccess(e); // LinkedHashMap 实现,跟 ArrayList 没关系
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); // LinkedHashMap 实现,跟 ArrayList 没关系
    return null;
}

转红黑树

细节后面再揪,现在先点到为止吧。树的变种很多,二叉,多叉,二叉又分为完全、完满、平衡、AVL、红黑,目前还不具备这块能力 _

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) // 如果数组长度没有达到 64
        resize(); // 扩容,并不会转红黑树
    else if ((e = tab[index = (n - 1) & hash]) != null) { // 当前节点取出来赋值给 e,e 就是当前节点,这个链表的节点要维护成红黑树的节点
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null); // 这里把 e 转成红黑树节点
            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);
    }
}

扩容

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 老的长度
    int oldThr = threshold; // 老的扩容阈值
    int newCap, newThr = 0; // 新的长度和扩容阈值
    // 如果数组不为空(不是初始化的扩容)
    if (oldCap > 0) {
        // 如果数组长度已经达到 MAXIMUM_CAPACITY(最大值)
        if (oldCap >= MAXIMUM_CAPACITY) { 
            threshold = Integer.MAX_VALUE; 
            return oldTab; // 达到了最大值,不会再扩容,直接把原来的数组返回去
        }
        // 如果老的长度达到 16,新长度就是老长度的 2 倍,oldCap << 1 就是 * 2 的意思
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 如果新的长度没达到 MAXIMUM_CAPACITY,扩容阈值也扩大到两倍
            // 如果新长度没达到最大值,不设置扩容阈值,下一次再扩容的时候走上面的判断,赋值为 Integer.MAX_VALUE
            newThr = oldThr << 1; 
    }
    // 有参构造创建的 HashMap,长度设置为根据参数计算出来的扩容阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr; 
    else {  // 无参构造创建的 HashMap,长度设置为 16,扩容阈值设置为 16*0.75 = 12
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) { // 带了参数创建的 HashMap 前面只设置了长度,这里设置下下一次的扩容阈值
        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; // 新数组赋值给 table
    if (oldTab != null) { // 如果老数据有数据,要把老数组的数据迁移到新数组
        for (int j = 0; j < oldCap; ++j) {
            // 因为长度变化了,原来数据对应的下标可能发生变化,会根据数组长度重新计算下标
            ... 具体代码就不贴了
        }
    }
    return newTab;
}
posted @ 2023-05-24 17:06  CyrusHuang  阅读(25)  评论(0编辑  收藏  举报