HashMap详解

1.什么是哈希表

进行增删改查操作时,使用哈希函数进行定位,在不考虑哈希冲突的情况下,其操作的时间复杂度都为O(1)
(哈希函数:存储位置=f(关键字))

2.哈希冲突(碰撞)

定义:使用哈希函数进行定位时,不同的元素映射到了相同的地址
解决办法:a.开放定址法(发生冲突,继续寻址) b.再散列函数法 c.链地址法(数组+链表)

HashMap使用的就是数组+链表的结构transient Node<K,V>[] table

3.HashMap核心字段

size:实际存储的key-value对的个数
capacity:容量(数组的长度)
loadFactor:负载因子
threshold:阈值(容量*负载因子),size达到阈值之后就要对Map进行扩容

4.put方法

jdk1.7版本的
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;
}

1.判断table是否为空数组,若为空,则依据threshold的值对table进行初始化
2.判断key是否为null,若为null,则存在table[0]或table[0]的冲突链上
3.针对key值,调用一系列函数,找到该元素要put在table数组中的实际位置i,定位到table[i]这条冲突链
4.遍历table[i]这条冲突链,判断要进行put的元素是否已存在,若已存在,则使用新value替换旧value并返回旧value
5.新增一个entry

jdk1.8版本的

HashMap使用的是链表来解决哈希冲突的,在数组上进行定位时使用的哈希函数,时间复杂度是O(1),但是链表上的操作还是使用的是遍历链表的方式,时间复杂度是O(n),当一个链表上挂载了大量的元素时,Map的性能就会大打折扣。为了解决这个问题,jdk1.8中当某一个链表上的元素数量大于某个数值(默认为8)时,会将改条链表转为红黑树结构

public V put(K key, V value) {
  //调用putVal方法 在此之前会对key做hash处理
  return putVal(hash(key), key, value, false, true);
}

//进行添加操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  //如果当前数组table为null,进行resize()初始化
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  //(n - 1) & hash 计算出下标 如果该位置为null 说明没有碰撞就赋值到此位置
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else {
    //反之 说明碰撞了  
    Node<K,V> e; K k;
    //判断 key是否存在 如果存在就覆盖原来的value  
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    //没有存在 判断是不是红黑树
    else if (p instanceof TreeNode)
      //红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    //都不是 就是链表 
    else {
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          //将next指向新的节点
          p.next = newNode(hash, key, value, null);
          //这个判断是用来判断是否要转化为红黑树结构
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        // key已经存在直接覆盖value
        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;
}

5.扩容的过程

当进行addEntry的操作时,如果size>=threshold时,进行扩容操作,扩容操作将table的容量扩大到现在容量的两倍。
这样的扩容方式确保HashMap数组的长度一定是2的次幂,这样既保证了新老数组索引相同,又使数组索引index更加均匀。

6.线程安全问题

若存在线程A和线程B同时往HashMap中进行put操作,在使用哈希函数寻址时,若定位到了数组的同一个位置,可能会出现某个线程的put操作覆盖掉前一个线程put操作的数值,因此HashMap是线程不安全的。

参考文章
https://segmentfault.com/a/1190000017362670
https://segmentfault.com/a/1190000013650892

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值