HashMap-源码

2023/9/9

# put

        HashMap map =new HashMap(2);
        map.put("li",1);
        map.put("c",2);
        map.put("c",3);

# JDK1.8源码:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
/**
 * Implements Map.put and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //transient Node<K,V>[] table;第一个if里面有个table,这个是存放map的数组
    //用于下面if里面进行赋值
    Node<K,V>[] tab; 
    //节点,用于临时存放map里的节点
    Node<K,V> p; 
    //n 数组大小,i 插入元素在数组中的下标位置
    int n, i;
    //tab = table进行赋值,判断tab是否空,如果空,进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
    //进行数组初始化大小  n
        n = (tab = resize()).length;
    //(n - 1) & hash] 通过hash运算找到插入元素应该放到的数组下标
    //p = tab[i = (n - 1) & hash]将数组这个下标的元素赋值给p
    //判断p是否是空,代表没有元素,没有hash冲突
    if ((p = tab[i = (n - 1) & hash]) == null)
    //直接创建一个新节点,放到数组中
        tab[i] = newNode(hash, key, value, null);
    else {
    //代表这个数组已经有元素了,需要解决hash冲突
    //说明下面的数据是链表或者红黑树,创建Node<K,V> e来临时存放元素
        Node<K,V> e; 
        K k;
        //判断桶里的元素和插入元素的key和hash是否都相等,这里判断的是tab[i]
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //如果相等,进行覆盖
            e = p;
        //不相等的情况就新增
        //判断链表是否为红黑树
        else if (p instanceof TreeNode)
        //如果是红黑树,直接进行put添加
            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);
                    //插完之后,如果链表元素个数大于等于8,链表转换成红黑树
                    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))))
                    //如果相等,就跳出,让下面if (e != null) { 开始覆盖
                    break;
                // 说明没有hash冲突,继续遍历链表吧    
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        //桶里的元素不为空
        if (e != null) { // existing mapping for key
        //记录一下桶里的元素
            V oldValue = e.value;
            //onlyIfAbsent为false:改变原来的值
            //oldValue == null 旧值为空
            if (!onlyIfAbsent || oldValue == null)
            //覆盖
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //修改++
    ++modCount;
    //判断数组是否需要扩容 
    //size:The number of key-value mappings contained in this map.链表长度
    //threshold:The next size value at which to resize (capacity * load factor).
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

# resize扩容

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //原数组大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧阈值点 默认是12(16*0.75)
    int oldThr = threshold;
    
    int newCap, newThr = 0;
    if (oldCap > 0) {
   // static final int MAXIMUM_CAPACITY = 1 << 30;数组最大容量,如果超过了,就不扩容了,你就去碰撞吧,我也没法
        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;
}

# 为什么说hashmap的扩容机制很妙?

# 1.位运算

HashMap使用位运算来计算hash值与数组长度之间的关系。HashMap内部,数组的长度总是2的幂次方,这使得数组的下标计算更加高效。通过哈希值的进行位与运算(hash&(length-1)),可以将哈希值映射到数组的索引位置上,由于数组长度总是2的幂次方,所以length-1的二进制表示所有的位都是1,这样就保证了位与运算的结果为哈希值的低位,从而实现快速定位。

1.为什么数组的长度是2的幂次方,数组下标的计算会更高效?2.length-1的二进制表示所有的位都是1为什么?3.哈希值的低位,从而快速定位,为什么?

  1. 数组的长度选择为2的幂次方是为了利用位运算来计算元素的索引位置,这样可以提高计算效率。假设数组的长度为2的幂次方n,那么n-1的二进制表示为所有低位都是1,高位都是0。这样,在使用哈希值对数组长度取模时,就相当于对哈希值的低位进行了取模操作,这样可以确保散列到各个位置的概率均等,减少了碰撞的可能性。
  2. 当数组的长度为2的幂次方n时,n-1的二进制表示中的所有位都是1。这是因为一个2的幂次方减去1的结果,其二进制表示中的每个位均为1。这样做有利于通过位运算来计算元素的索引位置,提高了计算效率。(参考下方进制对照表)
  3. 哈希值的低位被用来快速定位是因为在进行取模运算时(%操作),取模运算实际上就是保留了除数的低位,而丢弃了高位。当数组长度为2的幂次方时,取模运算实际上就是对哈希值进行与(&)操作,这样可以直接使用哈希值的低位来确定元素存放的位置,而不需要进行昂贵的除法运算。这种技巧可以提高散列表的性能,特别是在处理大量数据时。

# 2.新增位0不变

新增位为0不变:在HashMap的扩容过程中,当一个元素需要插入到已有的数组中时,跟进元素的哈希值重新计算在新数组中的索引位置,如果新数组的长度是原数组的两倍,那么新数组的长度的二进制表示中只有最高位不同,其他位都是相同的,因此,在重新计算索引位置时,只需要判断原索引位置的最高位是0还是1,如果是0,则索引位置不变 ,如果是1,则在原索引位置的基础上加上原数组的长度的值。这个操作巧妙的运用的位运算,避免的大量元素的重新计算和拷贝,提高了扩容的效率。

# 举个例子

当HashMap的容量达到一定阈值时,就会触发扩容操作。下面以一个简单的例子来详细说明HashMap的扩容过程。

假设我们有一个HashMap,初始容量为8,负载因子为0.75(即在HashMap中存储的元素数量超过容量的75%时,就会进行扩容)。现在我们向HashMap中插入5个元素,它们的哈希值分别为3、11、19、27和35(为了方便说明,这里直接使用元素的哈希值作为示例)。

  1. 初始状态:

    • 容量:8
    • 元素数量:0
  2. 插入第一个元素(哈希值为3):

    • 根据哈希值计算索引位置:3 & (8-1) = 3
    • 在索引位置3处插入元素
  3. 插入第二个元素(哈希值为11):

    • 根据哈希值计算索引位置:11 & (8-1) = 3
    • 发生了冲突,需要处理冲突
    • 发现索引位置3已经被占用,通过链表方式存储冲突的元素
    • 在索引位置3的链表尾部插入元素
  4. 插入第三个元素(哈希值为19):

    • 根据哈希值计算索引位置:19 & (8-1) = 3
    • 发生了冲突,需要处理冲突
    • 发现索引位置3已经被占用,通过链表方式存储冲突的元素
    • 在索引位置3的链表尾部插入元素
  5. 插入第四个元素(哈希值为27):

    • 根据哈希值计算索引位置:27 & (8-1) = 3
    • 发生了冲突,需要处理冲突
    • 发现索引位置3已经被占用,通过链表方式存储冲突的元素
    • 在索引位置3的链表尾部插入元素
  6. 插入第五个元素(哈希值为35):

    • 根据哈希值计算索引位置:35 & (8-1) = 3
    • 发生了冲突,需要处理冲突
    • 发现索引位置3已经被占用,通过链表方式存储冲突的元素
    • 在索引位置3的链表尾部插入元素

此时,HashMap的状态如下:

  • 容量:8
  • 元素数量:5
  • 索引位置3上有一个包含5个元素的链表,元素的顺序为3 -> 11 -> 19 -> 27 -> 35
  1. 扩容操作触发:

    • 当元素数量达到容量的0.75倍时,触发扩容
    • 容量翻倍,变为16
    • 创建一个新的容量为16的数组,将旧数组中的元素重新分配到新数组中
  2. 重新分配元素:

    • 遍历旧数组中的元素,根据元素的哈希值计算在新数组中的索引位置
    • 哈希值为3的元素,新索引位置仍然为3,放在新数组的索引位置3处
    • 哈希值为11的元素,新索引位置为11 & (16-1) = 11,放在新数组的索引位置11处
    • 哈希值为19的元素,新索引位置为19 & (16-1) = 3,发生冲突
    • 发现索引位置3已经被占用,通过链表方式存储冲突的元素
    • 在索引位置3的链表尾部插入元素
    • 哈希值为27的元素,新索引位置为27 & (16-1) = 11,发生冲突
    • 发现索引位置11已经被占用,通过链表方式存储冲突的元素
    • 在索引位置11的链表尾部插入元素
    • 哈希值为35的元素,新索引位置为35 & (16-1) = 3,发生冲突
    • 发现索引位置3已经被占用,通过链表方式存储冲突的元素
    • 在索引位置3的链表尾部插入元素

此时,HashMap的状态如下:

  • 容量:16
  • 元素数量:5
  • 索引位置3上有一个包含3个元素的链表,元素的顺序为3 -> 19 -> 35
  • 索引位置11上有一个包含2个元素的链表,元素的顺序为11 -> 27

通过这个例子,我们可以看到在HashMap扩容过程中,通过重新计算哈希值的索引位置,将元素重新分配到新的数组中。对于哈希值相同的元素,会通过链表方式存储,保持了元素之间的顺序。这样,HashMap的扩容操作能够保证元素的迁移效率,并且保持了元素的相对顺序。

# ConcurrentHashmap和hashmap的区别

  1. 线程安全性
    • HashMap线程不安全、ConcurrentHashMap是线程安全的。如果多个线程同时操作一个 HashMap 对象,可能会导致数据不一致或者抛出 ConcurrentModificationException 异常。
    • ConcurrentHashMap使用锁分段技术(Segment)(jdk1.7)或CAS(jdk1.8)保证线程安全。不同的部分可以被不同的线程同时访问,从而提高了并发性能。
  2. 并发性能
    • 多线程环境下,ConcurrentHashMap 的性能通常优于 HashMap,因为它允许多个线程同时访问数据,而写操作只会锁住对应的部分,不会影响其他部分的读操作
    • HashMap在多线程环境下需要自行保证线程安全,要额外的同步手段,会降低性能
  3. 扩容机制
    • HashMap在扩容时需要重新计算hash,重新分布数据,整个过程非线程安全,可能导致链表的循环引用问题
    • ConcurrentHashMap在扩容时,只需锁住部分,其他部分依然可以被访问,因此扩容时性能更好

# HashMap扩容可能出现循环引用问题

在HashMap的扩容过程中,由于需要重新计算哈希值并重新分布数据,可能会导致链表循环引用的问题。具体来说,当存在多个元素哈希值映射到同一个索引位置,并形成链表时,在扩容过程中,如果没有正确处理链表节点的引用关系,就可能导致链表的循环引用。

举个例子来说明这个问题:

假设我们有一个初始容量为8的HashMap,负载因子为0.75,存储的元素如下:

  • 元素A的哈希值为3,存储在索引位置3
  • 元素B的哈希值为11,存储在索引位置3,并与元素A形成链表

此时,HashMap的状态如下:

  • 容量:8
  • 元素数量:2
  • 索引位置3上有一个包含2个元素的链表,元素的顺序为A -> B

当元素数量达到容量的0.75倍时,触发扩容操作。容量翻倍,变为16,新建一个容量为16的数组,并将旧数组中的元素重新分配到新数组中。

在重新分配元素的过程中,我们需要遍历旧数组中的链表,并根据元素的哈希值计算在新数组中的索引位置。假设在索引位置3的链表上进行处理,我们需要重新计算元素B的哈希值,并尝试将其放入新数组中。

如果在重新计算哈希值的过程中,没有正确处理链表节点的引用关系,就可能出现链表循环引用的问题。具体来说,假设重新计算元素B的哈希值后,它的新索引位置仍然为3。如果在将元素B插入新数组的索引位置3时,没有断开旧数组中元素B与元素A之间的引用关系,就会导致链表循环引用的问题。

这样,新数组中的索引位置3上的链表会变成一个循环链表,元素A的下一个节点是元素B,元素B的下一个节点是元素A,形成了一个闭环。这种循环引用会影响HashMap的正常操作,可能导致无限循环、数据丢失等异常情况发生。

为了解决这个问题,HashMap在扩容过程中需要正确处理链表节点的引用关系,确保在重新分配元素时不会产生链表循环引用的问题。

进制对照表: