hashmap添加元素的源码分析

//HashMap的主干数组,可以看到就是一个Node数组,初始值为空数组,主干数组的长度一定是2的次幂
transient Node<k,v>[] table;</k,v>
从源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,
根据hash值得到这个元素在数组中的位置(即下标),
如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
如果数组该位置上已经存放有其他元素了,
根据哈希表中元素个数确定是扩容还是树形化
如果是扩容
那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
如果是树形化
1.遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
2.然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容

HashMap 中往红黑树中添加一个新节点 n 时,有以下操作:
1.从根节点开始遍历当前红黑树中的元素 p,对比 n 和 p 的哈希值;
2.如果哈希值不相等,根据当前节点与要添加的节点的哈希值的对比结果,确定往左还是往右添加
并且要保证当前节点还没有左孩子或者右孩子时,才能插入到相应位置
3.如果当前节点和要添加的节点的哈希值相等并且键也相等,就判断为已经有这个元素
(这里不清楚为什么不对比值--键不能重复);
4.如果当前节点和要添加的节点哈希值相等,但是两个节点的键不是一个类,只好去挨个对比左右孩子
5.如果从孩子所在子树中可以找到要添加的节点,就直接返回找到的节点,就判断为已经有这个元素
6.如果还是找不到,就通过其他信息进行比较,比如引用地址来给个大概比较结果,
这里可以看到红黑树的比较并不是很准确,注释里也说了,只是保证个相对平衡即可;
7.最后得到哈希值比较结果后,如果当前节点 p 还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环;
8.插入元素后还需要进行红黑树例行的平衡调整,还有确保根节点的领先地位。
---------------------

static class Node<K,V> implements Map.Entry<K,V> {
    //哈希值,就是位置
    final int hash;
    //
    final K key;
    //
    V value;
    //指向下一个几点的指针
    Node<K,V> next;
    //...
}

 


树节点:

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

 

    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是否为空,
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//创建一个新的table数组,并且获取该数组的长度
        //根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//如果对应的节点存在
            Node<K, V> e;
            K k;
            //判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
            else if (p instanceof TreeNode)
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            else {// 该链为链表
                //遍历table[i],判断链表长度是否大于TREEIFY_THRESHOLD(默认值为8),大于8的话把链表转换为红黑树,
                //在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
                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;
        // 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

    final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
        Class<?> kc = null;
        boolean searched = false;
        TreeNode<K,V> root = (parent != null) ? root() : this;
        //每次添加元素时,从根节点遍历,对比哈希值
        for (TreeNode<K,V> p = root;;) {
            int dir, ph; K pk;
            if ((ph = p.hash) > h)
                dir = -1;
            else if (ph < h)
                dir = 1;
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            //如果当前节点的哈希值、键和要添加的都一致,就返回当前节点(奇怪,不对比值吗?)
                return p;
            else if ((kc == null &&
                    (kc = comparableClassFor(k)) == null) ||
                    (dir = compareComparables(kc, k, pk)) == 0) {
                //如果当前节点和要添加的节点哈希值相等,但是两个节点的键不是一个类,只好去挨个对比左右孩子
                if (!searched) {
                    TreeNode<K,V> q, ch;
                    searched = true;
                    if (((ch = p.left) != null &&
                            (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                                    (q = ch.find(h, k, kc)) != null))
                        //如果从 ch 所在子树中可以找到要添加的节点,就直接返回
                        return q;
                }
                //哈希值相等,但键无法比较,只好通过特殊的方法给个结果
                //这个方法用于 a 和 b 哈希值相同但是无法比较时,直接根据两个引用的地址进行比较
                //这里源码注释也说了,这个树里不要求完全有序,只要插入时使用相同的规则保持平衡即可
                dir = tieBreakOrder(k, pk);
            }
                //经过前面的计算,得到了当前节点和要插入节点的一个大小关系
                //要插入的节点比当前节点小就插到左子树,大就插到右子树
            TreeNode<K,V> xp = p;
                //这里有个判断,如果当前节点还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                Node<K,V> xpn = xp.next;
                TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                if (dir <= 0)
                    xp.left = x;
                else
                    xp.right = x;
                xp.next = x;
                x.parent = x.prev = xp;
                if (xpn != null)
                    ((TreeNode<K,V>)xpn).prev = x;
                //红黑树中,插入元素后必要的平衡调整操作
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
    }

 

posted on 2019-05-02 13:49  汪洋中的一条船  阅读(699)  评论(0编辑  收藏  举报