关于HashMap的详细分析参考博文:
深入学习Java集合之HashMap的实现原理
HashMap死循环分析

【1】HashMap内部数组何时创建?

绝不是初始化时创建,而是在第一次put方法调用的时候创建。咱们从源码分析。

① put方法入口

public V put(K key, V value) {
    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;
        //注意这里,table为null,就会调用resize进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            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))))
                e = p;
            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);
                        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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

第一步,创建一个HashMap,放入一个键值对:

public static void main(String[] args){
     HashMap map=new HashMap();
      map.put("1","1");
  }

如下图所示,此时HashMap变量是默认初始化。
【每日一面】关于HashMap
第二步,判断table为null,就会调用resize进行“显示初始化”:
【每日一面】关于HashMap
如下图所示,旧的临界值、容量都为0:
【每日一面】关于HashMap
默认初始化,容量为16,临界值为12:
【每日一面】关于HashMap
创建一个新的数组赋值给"table"变量–transient Node<K,V>[] table;
【每日一面】关于HashMap
此时HashMap中的transient Node<K,V>[] table;才是一个真正意义上的数组,然后将键值对1:1放进去。


【2】 HashMap内部table数组容量为什么是2^n?

第一点,未指定initialCapacity时,默认初始化容量为16,然后以后扩容为2倍扩容,从这个意义上来讲肯定是2^n。

那么指定了initialCapacity呢?比如指定3,这是数组的容量是多少?继续看源码。

源码示例如下:

public static void main(String[] args){
    HashMap map=new HashMap(3);
      map.put("1","1");
  }
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);
    }

可以看到这里调用了tableSizeFor(initialCapacity),这个方法如**释说明是使用给定的容量返回一个2^n的数。

 /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

此时数组还未初始化,只是根据容量计算了临界值–4。在第一次put时,将会调用resize进行初始化。

如下所示,根据指定的initialCapacity计算一个threshold 然后在这里进行判断并赋值给newCap:
【每日一面】关于HashMap
再根据得到的newCap(4)计算出newThr(3):
【每日一面】关于HashMap
int threshold;transient Node<K,V>[] table;重新赋值:
【每日一面】关于HashMap


【3】仔细分析tableSizeFor方法

可以发现tableSizeFor方法保证了无论指定了什么initialCapacity,都会返回一个2^n的数。当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)

下面我们仔细分析tableSizeFor方法。

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

① cap-1

这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。

n |= n >>> 1

无符号按位右移运算符。左操作数按位右移右操作数指定的位数,符号位跟着移动,空出来的高位补0(左边补充的值永远为0,不管其最高位(符号位)的值)。

先进行无符号右移一位(相当于n/2),然后再与n进行|运算(输入2个参数,a、b对应位只要有一个为1,c对应位就为1;反之为0)。

由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。

n |= n >>> 2;

注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。

④ n |= n >>> 4;

这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。

以此类推

注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就31个1(int最大正数值),但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY

static final int MAXIMUM_CAPACITY = 1 << 30;

图例如下:
【每日一面】关于HashMap


【4】hash()方法分析

源码基于jdk1.8:

static final int hash(Object key) {
    int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

从上面的代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)

【每日一面】关于HashMap

为什么要这么干呢? 这个与HashMap中table下标的计算有关。

n = table.length;
index = (n-1) & hash;

因为,table的长度都是2的幂,因此index仅与hash值的低n位有关(此n非table.leng,而是2的幂指数),hash值的高位都被与操作置为0了。

假设table.length=2^4=16和某散列值做“与”操作如下,结果就是截取了最低的四位值。

    10100101 11000100 00100101
&   00000000 00000000 00001111
----------------------------------
    00000000 00000000 00000101    //高位全部归零,只保留末四位

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比烦人。

时候“扰动函数”的价值就体现出来了,看下面这图:

【每日一面】关于HashMap

右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

参考博文:HashMap源码注解 之 静态工具方法hash()、tableSizeFor()

相关文章:

  • 2021-12-11
  • 2022-01-13
  • 2021-09-22
  • 2021-08-06
  • 2021-07-31
  • 2021-08-01
猜你喜欢
  • 2021-04-27
  • 2021-11-26
  • 2022-02-04
  • 2021-12-02
  • 2021-08-22
相关资源
相似解决方案