我们通过面试中常见的几个问题来谈谈

描述一下putVal的过程

  • 数据结构:数组加链表加红黑树
  • 如何确定添加的元素在底层数组的哪个位置? tab[i = (n - 1) & hash] n就是数组长度
  • HashMap初始化大小是多少,为什么是16?
  • 出现冲突了怎么处理?
  • 怎么扩容?
  • 为什么2倍扩容
  • 为什么要进行树化?

hash

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

table

transient Node<K,V>[] table;
  • 1.

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)
            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;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

第一次使用HashMap添加数据的时候底层会创建一个长度为16的默认Node数组。

  • 为啥初始化大小是16 ?
  • 为什么容量要是 2 的整数次幂?

计算下标

  • i = (n - 1) & hash
    • n就是数组长度
    • 这里是做位与运算
  • 为什么要进行取模运算以及位运算
    • 比如这里默认是一个长度为16的Node数组,
      我们现在要根据传进来的key计算一个下标值出来然后把value放入到正确的位置,
      想一下,我们用key的hashcode与数组长度做取模运算,
      得到的下标值是不是一定在数组的长度范围之内,
      也就是得到的下标值不会出现越界的情况。
  • 为啥不是取模运算而是位与运算呢?
    • 使用位与运算的一方面原因就是它的性能比较好,
      另外一点就是这里有这么一个等式:(n - 1) & hash = n % hash
  • 为什么要减一做位运算
    • 这里的n-1是为了实现与取模运算相同的效果
    • 2的整数次幂减一得到的数非常特殊
  • 这样得到的下标值就是均匀分布的啊,那冲突的几率就减少啦

为什么使用尾插法?

  • 使用头插法会改变链表的顺序
    如果扩容的话,由于原本链表顺序有所改变,扩容之后重新hash,
    可能导致的情况就是扩容转移后前后链表顺序倒置,
    在转移过程中修改了原来链表中节点的引用关系。
  • 因为扩容前后链表顺序是不变的,他们之间的引用关系也是不变的。

扩容

  • 首先是创建一个新的数组,容量是原来的二倍
  • 然后会经过重新hash,把原来的数据放到新的数组上,
    至于为啥要重新hash,那必须啊,你容量变了,相应的hash算法规则也就变了,得到的结果自然不一样了。

链表与树的转化

  • 在Java8之前是没有红黑树的实现的,在jdk1.8中加入了红黑树,
    就是当链表长度为8时会将链表转换为红黑树,
    为6时又会转换成链表,这样时提高了性能,也可以防止哈希碰撞。

resize

  • resize方法的主要作用就是初始化和增加表的大小,说白了就是第一次给你初始化一个Node数组,其他需要扩容的时候给你扩容

为什么不是线程安全的?
什么造成它是线程不安全的?
如何解决?
解决方案不同?

  • 当多个线程同时执行 put 操作时,若键的哈希值相同,可能导致链表或红黑树节点的覆盖或丢失。
    示例:两个线程同时插入不同的键值对,哈希到同一桶位置。若未同步,后一个线程的写入可能覆盖前一个线程的结果。

了解更多  java基础:目录索引