【Java容器】(5)HashMap

基础

  1. HashMap中对相同对象的判定:当且仅当hashCode一致,并且equals一致的对象,才会被HashMap认定为同一个对象。
  2. Hash冲突
哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的哈希值。这时候就产生了哈希冲突。

如果多个对象被Hash函数算出来的地址(桶索引)相同,那么这些对象就发生了Hash冲突,好的Hash函数会尽量避免发生Hash冲突。

  1. HashMap解决Hash冲突的方法:链式地址法,对于相同的Hash值,使用链表进行连接,然后使用数组储存链表。

Java8之前的HashMap

1.底层实现

数组+链表

2.成员变量和常量

  • transient Entry<K,V>[] table;
    table储存的是Entry数组,每个Entry是一个Hash桶,每个Entry都对应一条链表,hash冲突的数据,都会被放到同一条链表中。
  • transient int size
    键值对的实际数量
  • final float loadFactor
    负载因子,决定table的扩容量
  • int threshold
    确定是否需要扩容的阈值
  • static final int DEFAULT_INITIAL_CAPACITY=16
    初始化容量,默认为16,必须为2的幂
  • static final int MAXIMUM_CAPACITY = 1<<30
    最大容量
  • static final float DEFAULT_LOAD_FACTOR=0.75f
    默认负载因子

3.put(K,V)方法

public V put(K key, V value) {
    //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
   //如果key为null,存储位置为table[0]或table[0]的冲突链上
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
    int i = indexFor(hash, table.length);//获取在table中的实际位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
    addEntry(hash, key, value, i);//新增一个entry
    return null;
}
  • 流程:使用hash(key)方法计算key的Hash值,使用indexFor(hash,table.length)方法计算存入数组的索引,如果索引位置没有元素,就使用addEntry方法添加键值对,如果有元素,就沿着链表查找,如果key是相同的,就更新值,不相同的话,使用addEntry方法添加键值对。
  • hash(Object key)
    该方法计算key的hash值,hash算法应当尽可能的使哈希值分散,这样可以减少hash冲突,将元素尽可能的放在不同的hash桶中,即满足松散哈希
  • indexFor(hash,table.length)
    该方法根据hashcode和table长度计算该元素应当放在table数组的哪个索引位置,hash值小于length-1时,返回hash,大于length-1时,取余数,由于取余数的计算成本较高,因此hashmap规定table的长度为2的幂,这样可以通过h&(length-1)即可取余数。
  • addEntry(hash,key,value,index)
    该方法在指定索引位置添加键值对,如果键值对将要占用的位置不是null,并且size>=threshold,就会启动扩容方法resize(length),table的长度扩容为之前的2倍。
  • resize()
    根据新的容量,确定新的threshold的值,简单来讲,新的threshold的值 = 新容量 * loadFactor,然后根据uesAltHashing判断是否需要进行Hash重构,最后使用transfer方法重新计算所有键值对的新的数组索引。

问题:loadFactor对HashMap的影响?

负载因子的大小对HashMap的影响很大,太小了会导致HashMap频繁扩容,太大了会导致空间的浪费,因此需要在时间和空间上进行权衡,0.75是Java提供的建议值。

4.get(K)方法

  • 流程:先找到key对应的索引,然后沿着链表找对应的键值对
  • 可以看出get方法的性能关键在链表的长度上

5.性能分析

  • 在进行put操作时,消耗资源的操作是遍历链表扩容数组
  • 在进行get操作时,消耗资源的操作时遍历链表
    因此针对遍历链表操作,Java8中进行了优化。

Java8的HashMap

1.底层实现

数组+链表+红黑树

2.hash(key)方法

进行了简化,之前hash计算比较复杂是为了将hash值尽量分散,但我们注意到,HashMap的效率问题还是出在链表部分的遍历上,因此主要需要提高链表部分的遍历效率

3.putVal()方法

  • 第一步,计算下标,与之前一致
  • 第二步,当size超过threshold时,需要扩容
  • 第三步,保存数据,分为3种情况
  1. 下标位置为null,直接添加一个链表节点
  2. 下标位置为树节点,添加或覆盖一个树节点
  3. 下标位置为链表节点,覆盖或添加一个链表节点,但在添加链表节点时,需要判断链表长度如果为7(建树阈值-1),那么需要调用treeifyBin方法,将链表转换为红黑树.

问题:为什么选择8作为建树阈值(TREEIFY_THRESHOLD)?

链表的查找是O(n),红黑树的查找是O(logn),在n太小的时候,它们的查找效率相差不大。

4.resize方法

//todo:还没太懂

  • 第一步,和之前一样,重新规划table长度和阈值
  • 第二步,重新排列数据结点:遍历table上的每个结点
  1. 如果结点为null,不进行处理
  2. 不为null,没有next结点(只有1个结点在这个索引),重新计算hash值,存入新的table
  3. 为树结点,调用该树结点的split方法处理,对红黑树进行调整,如果红黑树太小,就将其退化成链表
  4. 链表结点:对于hashcode超出旧容量和未超出旧容量的链表分别处理,对于hash&oldCap==0的部分,不需要处理,反之,利用公式:新索引 = 原索引+旧容量,放到新的下标位置上。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值