HashMap详解(1)

139 阅读11分钟

1.创建一个HashMap

new HashMap(size)

    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;
        // 初始threshold     
        this.threshold = tableSizeFor(initialCapacity);
    }    
    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;
    }

这个tabSize方法是为了,寻找一个比当前传入cap大的且最近的2的n次幂数,以保证tab的size永远为2的n次幂。那么他是如何保证的呢?

首先我们给出一个随便的int数字,010000000000001011

那么在经过如下运算

0    1    0    0    0    0    0    0    0    0    0    0    0    0    1    0    1    1

0    1    0    0    0    0    0    0    0    0    0    0    0    0    1    0    1    1

................................................................................................................................

0    1    1    0    0    0    0    0    0    0    0    0    0    0    1    1    1    1    1

0    1    1    0    0    0    0    0    0    0    0    0    0    0    1    1    1    1    1

..................................................................................................................................

0    1    1    1     1    0    0    0    0    0    0    0    0    0    1    1    1    1    1    1    1

0    1    1    1     1    0    0    0    0    0    0    0    0    0    1    1    1    1    1    1    1

........................................................................................................................................................

0    1    1    1     1    1    1    1     1    0    0    0    0    0    0    0    0    0    1    1    1    1    1    1    1

我们直到一个int最大是32个比特位所以最大只需要位移5次,可以使输入的任何一个int值,只有一个最高位为1其余全为0。那么在加1后则成为一个2的n次幂。

但是为什么第一行需要对cap减去1呢?

因为我们的目的是为了寻找一个数,它比输入值大或等于,且是2的n次方数,但是如果当输入的数本身则是2的n次方幂。那么理应被采纳,因为我们的策略是将输入值转变为全1的二进制,最后在加1通过进位即可完成。如果输入一个8 则是 1 0 0 0 ,右移过后则是 1 1 1 1 在加1 则是 1 0 0 0 0 0则是16了这违背了我们的初衷。所以不论输入值是的多少则先减去1,以破坏他是2的n次幂的特性。但是-1之后变成了2的n次幂该如何呢?没关系因为我们只找 大于等于的。

2. PUT A VALUE

2.1 put
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
      }
    // 这里右移16位是为了再异或hash本身是为了,将高16位的信息混淆至低16位。使hash更加散列
    // 避免边界原因,导致高16位信息无法参加计算
     static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    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为空则调用resize初始化tab
       if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    // 如果p = tab[i = (n - 1) & hash] == null说明桶中没有相同hash的key
    // 直接在计算所得下表进行赋值杰克
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; // 此处的e是是否进入覆盖值得关键  K k;
            if (p.hash == hash &&
    // hash相同key也相同,没有出现hash碰撞
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
        // 代码进入此处说明右hash碰撞
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
        // 代码进入此处说明右hash碰撞
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                   // 出现hash碰撞则挂载链表
                        p.next = newNode(hash, key, value, null);
                        // 如果链表长度大于8则树化    
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 在挂再链表的过程中如果发现equals相同的key则break,进入
    // 随后的是否覆盖逻辑,因为到此处e必然不等于null,所以直接break
    // 如果链表存在循环这个链表时就会,检验链表中每个值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
    // 如果e不等于null说明没有出现hash碰撞,但是
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
    //onlyIfAbsent 决定是否覆盖
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount; // 增加修改次数
        // 记录元素个数,当达到扩容阈值时则直接resize扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

其实上述方法可以分为两大块来看,其一是未发生Hash碰撞,其二有发生Hash碰撞。那么对于1来说发生未碰撞,如果有相同key根据onlyIfAbsent决定是否覆盖。对于2来说,发生碰撞,挂链表,判断是否树化,判断已有链表中元素是否有相同得key,有则进入是否覆盖得逻辑。

2022-03-19-00-43-44-image.png 其整体逻辑

是否有相同发hash值

否------插入数组

是 ----- 是否有hash碰撞-

否----给e赋值根据onlyIfAbsent判断是否覆盖

是---挂连链表,判断是否有需要树华

是--------树化

否-------已有链表中是否有元素得key何插入key equals

是------给e赋值

否------继续循环

size++

判断是否需要扩容

2.2 树化
  final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;

//  如果桶容,也就是tab得size小于64时,其实进行树化并无太大意义,因为tab得size
// 过小必然会产生更多得碰撞,则进行tab数组得扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {

  // 逻辑走至次数说明桶容已大于64开始树华
            TreeNode<K,V> hd = null, tl = null;
            do {

  // 循环链表将普通得node变为treeNode链表
                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);、

  // 当转化完毕后此时其实转化为一颗o(n)时间复杂度得树
            if ((tab[index] = hd) != null)

  // 此处进一步转化为红黑树
                hd.treeify(tab);
        }
    }


 final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;

  // 每向树中插入一个数据,则需进行旋转,染色,使红黑树不断保持
// 无限区域o(logn)得时间复杂度
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }  

上述得树化步骤,其实也很清晰,首先将单向链表转化为一个时间复杂度为o(n)得tree。随后遍历得tree,形成红黑树,在向红黑树中插入元素时,每插入一次,通过balanceInsertion方法进行旋转染色平衡。

其实hashmap使用红黑树,而非avl树,就是为了避免作为强定义二叉树的avl,在插入元素时平凡旋转导致的性能损耗。而红黑树最多只需要旋转三次即可解决一切平衡问题。在插入性能上要优越许多,同时也能通过红黑树的四种特性,保证o(logn)的时间复杂度。

3. resize()方法详析

其实对于resize方法不仅仅负责对Node<K,V>[] table;的扩容操作,也负责对其进行初始化操作,也就是说 我们在new HasMa()/new HashMap(size)时,不论是前者还是后者,都不会产生tab数组,而tab数组正是在第一次插入元素时调用resize()进行初始化初始化。

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // ! 判断是否有oldCap
       if (oldCap > 0) {
        // 说明tab已近初始化
           if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
         // 此处不管如何都会将oldCap 扩容两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
         // 如果 oldCap已经大于16了则将 threadHold扩容两倍
                newThr = oldThr << 1; // double threshold
        }
         // 判断创建时是否指定threadHold,如果是则给cap赋值上threadHold的值
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 指定了threadHold但是没有初始化tab
             newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 既没有指定threadhold也没有
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
     // 创建时只指定了threadHold,走上上面逻辑的第二个
        if (newThr == 0) {
            // 给ThreadHold赋值 cap * lf
            float ft = (float)newCap * loadFactor;
            // 校验值是否合法,合法则赋值,不合法给int maxValue
            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;
     // 如果老的tab不等于null执行
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 如果head.next == null ,说明只有一个元素不是链表形式,直接重新hash填充
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                     // 如果head是null那么则树解构拆分
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    // head.next有值,说明头节点后面有值,是个链表,重新计算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;
                            }
                         // 使用重新调整的新下表
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }对上面的步骤进行总结:

流程:

上述的的代码大致可以分为两个个部分看

  • 首先通过if (oldCap > 0)可以判断出,当前调用resize方法的目的是初始化tab数组(false),还是对已有tab数组进行扩容(true)。如下图所示1操作则则是扩容部分的前置处理,而2则是进行数组的初始操作。下面则进行详细的分析。

2022-03-18-23-12-35-image.png

如果if(oldCap > 0)返回false,如上所说则进入扩容,也就是进入2步骤。如果oldThr > 0 返回true那么意味着,在new HashMap(size)时使用了有参构造器,只需要将初始值赋值给newCap(tab的size)即可。返回false则构造Map时使用的是无参构造器,给tab的size赋上初始值(16),并给扩容阈值附上初始值。而使用了有参构造的Map在上面的逻辑并没有赋扩容阈值,所以newThr 必然等于0,所以会在if (newThr == 0) {}代码中给其赋值。当昨晚这一切时则直接创建新的tab,并将tab返回,到此tab创建完毕。

  • 如果if(oldCap > 0)返回的是true,那么说扩容,在一代码中,会将容量扩容至原本的2倍。并且oldCap的大小如果大于16时,会将扩容阈值也扩大至原本的2倍。在经历完此步骤后,则直接开始创建新的数组。

既是扩容那么必然oldTab != null则会进入下面扩容的逻辑分支。

2022-03-19-00-43-44-image.png

在上面的循环中,对老的tab数组进行循环,重新计算下表,并将它们放入正确的位置。在扩容中具体也可分为三个部分,其一,如果e.next() 等于null,那么则说明此head并没有形成链表,只需重新计算根据hash计算出正确的下标,放入新的数组即可。head是一个树节点,则直接数进行拆分,重新计算下标。除上述两种可能之外,则head定是一个链表。此处hashMap进行了特殊处理。他将形形成的链表,分成两类,一类则是head在新数组中下标不变,另一类head在新数组中需要进行调整。那么他是如何做的呢?就引入了本方法最有意思的地方。 将head的hash & oldCap 如果等于0 则无需调整下表,原因如下所示

(oldCap在初始化时就已经保证一定是2的整数次方,那么对应的二进制数则是首位为1其余为0)

如果oldCap为16

image.png

则说明此hash第5个比特位为0,那么在扩容至32 之后也就是 1 0 0 0 0 0 ,此时使用hash & (cap - 1)进行下标计算时结果是一样的(扩容后计算结果最高位多了个0)。

image.png

通过上述的例子可以看出下标是未改变的,将这一个链表串成loTail。

那么如果hash & oldCap不等于0则说明hash值的第oldCap个元素是1,在来看看这种情况的处理。

image.png

此时扩容后,计算下表高位多了个1,也就是比原来下表多了2^4。此时下标该表将这些元素串成hihead。后续在填入新的数组时,loTail用原下标,而hihead用 原下标 + oldCap。随后将扩容后的tab返回。

4. get

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

   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) {
    // 如果该key的hash和cap下标&计算的下标处存在元素
            if (first.hash == hash && // always check first node
            // 如果key也euqals则直接返回o(1)    
            ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 逻辑走至此处说明虽然hash相同,但key不equals
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                // 如果头节点是树节点,则进行树查找o(logn)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 如果不是树则进行链表的遍历直至返回元素o(n)
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

    // 递归使用二分法查找
    final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
            return null;
        }

其实上述步骤的逻辑极其清晰,先判断该桶中是否相同hash值得元素,如果没有返回则直接返回,如果有则进一步判断key是否相等,如果相等则直接返回元素,如果不等,那说明此元素定是存在于链表或者树上的。先判断树上是否有元素,如果有可以在树上找到,则直接返回,反之继续遍历链表寻找。整体得代码逻辑是按照o1 - 0logn -logn得时间复杂度去写得。

5. remove

remove得核心操作如下

    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;
     // 首先判断桶中是否有相同hash得key
       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;
       // 此处肯定是由相同hash得key    
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
              // 如果key也equals则直接给node赋值
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
               // 如果head元素有next且是树,给node赋值
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
               // 此处肯定是遍历链表,寻找是否有相同equeals得key
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            // 找到直接给node赋值,破坏循环    
                            node = e;
                            break;
                        }
                        // p 指向得是目标值得上一个值
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // node是一个重要得标志,如果不等于null那么则说明可以在这个map中寻找到
            // 值
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    // 如果头节点是tree得化则从树种删除元素,并旋转染色
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    // 如果node == p 那么说明当前删除得节点在桶tab数组上
                    tab[index] = node.next;
                else    
                    // 逻辑进行至此,说明删除得node在链表上,将前一个节点得next指向删除节点得next即可
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }