Java中常见集合类——HashMap(上)

  HashMap的数据存储结构是一个 Node<K,V> 数组,每一个Node包含一个key-value键值对。(Java 7 中是 Entry<K,V> 数组,但结构相同)

它的存储结构是数组加链表的形式,如下图。

  数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,

  如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;

  如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;

                  对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。

  所以,出于性能考虑,HashMap中的链表出现越少,性能才会越好。

 

  HashMap 在 Java 8 中的实现增加了红黑树,当链表节点达到 8 个的时候,会把链表转换成红黑树,低于 6 个的时候,会退回链表。

  因为链表的时间复杂度是 n/2 ,红黑树时间复杂度是 logn ,当 n 等于 8 的时候, log8 要比 8/2 小,这个时候红黑树的查找速度会更快一些。

  链表节点个数大于等于 8 时,链表会转换成树结构;节点个数小于等于 6 时,树会转变成链表。

  Q:为什么转变条件 8 和 6 有一个差值?

  A:如果没有差值,那么如果频繁的插入删除元素,链表个数又刚好在 8 徘徊,那么就会频繁的发生链表转树,树转链表。

 

  HashMap 在jdk1.8 中,hash算法如下:

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

  首先获取对象的 hashCode() 值,然后将 hashCode 值右移16位,然后将右移后的值与原来的 hashCode 做异或运算,返回结果。

  (其中h>>>16,在JDK1.8中,优化了高位运算的算法,使用了零扩展,无论正数还是负数,都在高位插入0)。

 

  HashMap 通过 put & get 方法存储和获取。其中 put 方法通过 putVal()来插入元素,get方法通过 getNode()来取得元素。

public V put(K key, V value) {
    // 对key的hashCode()做hash 
    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;
    // 步骤①:tab为空则创建 
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤②:计算index,并对null做处理  
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 步骤③:节点key存在,直接覆盖value 
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // 步骤④:判断该链为红黑树 
        // hash值不相等,即key不相等;为红黑树结点
        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;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 步骤⑥:超过最大容量 就扩容 
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}
View Code

总结 HashMap put 过程

  1. 计算 key 的 hash 值。计算方式是 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

  2. 检查当前数组是否为空,为空需要进行初始化,初始化容量是 16 ,负载因子默认 0.75。

  3. 计算 key 在数组中的坐标。计算方式:(容量 - 1) & hash.因为容量总是2的次方,所以-1的值的二进制总是全1。方便与 hash 值进行与运算。

  4. 如果计算出的坐标元素为空,创建节点加入,put 结束。

       如果当前数组容量大于负载因子设置的容量,进行扩容。

  5. 如果计算出的坐标元素有值。

    5.1 如果 next 节点为空,把要加入的值和 key 加入 next 节点。

    5.2 如果 next 节点不为空,循环查看 next 节点。

    5.3 如果发现有 next 节点的 key 和要加入的 key 一样,对应的值替换为新值。

    5.4 如果循环 next 节点查找超过8层还不为空,把这个位置元素转换为红黑树。

    5.5 如果坐标上的元素值和要加入的值 key 完全一样,覆盖原有值。

    5.6 如果坐标上的元素是红黑树,把要加入的值和 key 加入到红黑树。

    5.7 如果坐标上的元素和要加入的元素不同(尾插法增加)。

图解:

  

注意:
    HashMap的put会返回key的上一次保存的数据,比如:

HashMap<String, String> map = new HashMap<String, String>();
System.out.println(map.put("a", "A")); // 打印null
System.out.println(map.put("a", "AA")); // 打印A
System.out.println(map.put("a", "AB")); // 打印AA

 参考:

  https://baijiahao.baidu.com/s?id=1662733945029523270&wfr=spider&for=pc

  https://blog.csdn.net/woshimaxiao1/article/details/83661464

  https://blog.csdn.net/moneywenxue/article/details/110457302

posted @ 2021-03-30 17:45  zjcfrancis  阅读(68)  评论(0编辑  收藏  举报