从JDK7与JDK8对比详细分析HashMap的原理与优化

本文深入探讨HashMap的工作原理,对比JDK7和JDK8的差异,包括put函数实现、hash与hashCode()、扩容策略,并分析并发场景下的问题。JDK8通过红黑树优化了链表过长的情况,减少了碰撞并提升了性能。

概述

从本文你可以学习到:

  1. 什么时候会使用 HashMap ?他有什么特点?
  2. 你知道 HashMap 的工作原理吗?
  3. 你知道 get 和 put 的原理吗?equals()hashCode() 的都有什么作用?
  4. 你知道 hash 的实现吗?为什么要这样实现?
  5. 如果 HashMap 的大小超过了负载因子(load factor)定义的容量,怎么办?
  6. 为什么 HashMap 的容量是2的 n 次幂的形式?

在说明这些问题的同时, 我从JDK7——JDK8的 HashMap 的变化来说明开发人员对这个数据结构的优化,重点放在了 put() 函数resize() 函数,还结合了《码出高效》这本书指出了 HashMap 在并发情况下表现出来的问题。

注意:源码可能与JDK中实际代码略有不同, 这里面JDK7版以《码出高效》为准,JDK8版本以网络版本为准,意在说明某个函数功能, 便于理解。在优快云上,不能上传bmp格式图片,大家感兴趣的话,访问我的个人博客,那里排版更清晰,图片展示更明了

两个重要参数说起

在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)

Capacity 就是 bucket 的大小,Load factor就是 bucket 填满程度的最大比例。如果对迭代性能要求很高的话,不要把 capacity 设置过大,也不要把 load factor 设置过小。当bucket中的entries的数目大于capacity*load factor 时,就需要调整 bucket 的大小为当前的2倍。

put函数的实现

put函数大致的思路为:

  1. 对key的 hashCode() 做 hash ,然后再计算 index ;
  2. 如果没碰撞直接放到 bucket 里;
  3. 如果碰撞了,以链表的形式存在 buckets 后;
  4. 如果碰撞导致链表过长(大于等于 TREEIFY_THRESHOLD ),就把链表转换成红黑树;
  5. 如果节点已经存在就替换 old value(保证key的唯一性)
  6. 如果 bucket 满了(超过 load factor * current capacity),就要resize。

具体代码的实现如下:

JDK7 的 put

public V put(K key, V value) {
   
   
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //此循环通过 hashCode 返回值找到对应的数组下标位置
    //如果 equals 结果为真,则覆盖原值, 如果都为 false ,则添加元素
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
   
   
        Object k;
        //如果key的 hash 是相同的,那么在进行如下判断
        //key 是同一个对象或者 equals 返回为真, 则覆盖原来的Value值
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
   
   
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
   
   
    //如果元素的个数达到 threshold 的扩容阈值且数组下标位置已经存在元素,则进行扩容
    if ((size++ >= threshold) && (null != table[bucketIndex])){
   
   
        //扩容 2 倍, size 是实际存放元素的个数,而 length 是数组的容量大小(capacity)
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

//插入元素时, 应该插入在头部, 而不是尾部
void createEntry(int hash. K key, V value, int bucketIndex){
   
   
    //不管原来的数组对应的下标是否为 null ,都作为 Entry 的 BucketIndex 的 next值
    Entry<K,V> e = table[bucketIndex];***)
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    size++;
}

关于并发的问题
如上源码, 在 createEntry() 方法中,新添加的元素直接放在 slot 槽( slot 哈希槽,table[i] 这个位置)使新添加的元素在下一次提取后可以更快的被访问到。 如果两个线程同时执行 (***) 处时, 那么一个线程的赋值就会被另一个覆盖掉, 这是对象丢失的原因之一。 我们构造一个 HashMap 集合,把所有元素放置在同一个哈希桶内, 达到扩容条件后,观察一下 resize() 方法是如何进行数据迁移的。示例代码和图可参考《码出高效》P204

JDK8 的 put

public V put(K key, V value) {
   
   
    // 对key的hashCode()做hash
    return putVal(hash(key), key, value, false
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值