Java 之 HashMap

283 阅读1分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。

  • HashMap 数组最大容量是 (1<<30) ::: tip HashMap 数组最大容量是(1<<30),但是 HashMap 的容量是没有限制的,因为 HashMap 是使用数组加链表结构存储的,每个数组里面存储的是链表的头结点(1.8 之后当链表长度超过 8,会用红黑树的形式存储),当产生 hash 冲突的时候会在链表或者树新增结点,所以容量不受限制。 ::: ::: warning 为什么数组最大容量设置为(1<<30),而不是整数的最大范围呢? :::

  • HashMap 是通过计算出来的 hashCode 和数组总容量取模运算获取数组下标的,但是由于取模运行速度要比位运算慢很多,HashMap 是通过如下方法进行运算从而达到取模运算的效果,即 a%b == (b-1)&a 当 b 是 2 的幂次方时成立

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  • HashMap 容量是 2 的幂次方的原因 ::: tip HashMap 在添加元素的时候会计算 hash 值,计算的时候会用(n-1)&hash,当 n 为 2 的幂次方的时候,(n-1)就是所有位全部都是 1,如果换成其他的值的话,转成二进制,必定会有位上面是 0 的,这个时候这个位置永远都不会被用到。这个时候产生碰撞的概率更高,减慢了查询速度。 :::

  • HashMap 扩容 ::: tip 当 HashMap 中元素越来越多的时候,碰撞的概率也就会越来越高,为了提高查询效率,这个时候就要对 HashMap 数组进行扩容了,当 HashMap 的元素个数超过了 数组大小*负载因子(0.75)的是偶会进行扩容 :::

  • 为什么我们极力避免 HashMap 扩容呢? ::: tip 很多时候我们都会建议估算 HashMap 容量大小,然后在创建 HashMap 的时候指定容量大小,就是为了避免 HashMap 扩容,因为 HashMap 扩容的代价很大,首先会新建一个 HashMap,然后把老的数据全部都重新计算 hash 值,计算在新的数组中的位置,放到新的数组中,这个过程是很耗时的,应该尽量避免这种情况。 :::

  • HashMap 不是线程安全的,如果必须用 HashMap 的话可以使用 Collections.synchronizedMap(new HashMap) 创建线程安全 HashMap

疑问

为什么 HashMap 数组容量最大为(1<<30)而不是(1<<31 - 1)这个正整数的最大值呢?

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

重新计算 HashMap 容量

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

Hashtable

ConcurrentHashMap

LinkedHashMap