再谈HashMap

在之前的一篇博客中,简单介绍了HashMap这种数据结构的基本原理,链接:聊聊HashMap。本问我还继续对HashMap中需要注意的地方再提一下。

我们先来看看HashMap的相关代码:

static class Node[] table;
static class Node<K,V> implements Map.Entry<K,V>{
  final K key;
  V value;
  Node<K,V> next;
  final int hash;
}

我们知道,在Java中,数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么我们可以综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构。这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为链表的数组 。

HashMap确认元素的存储位置以及put方法

我们知道哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢?一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。但是因为直接取模运算的消耗很大,HashMap中采用了效率更高的方法。它通过h&(length-1)来得到对象的保存位,h&(length-1)相当于对length的取模运算,HashMap底层数组的长度总是2的n次方,2的n次方减1得到的二进制数的每个位上的值都为1,那么与全部为1的进行与操作,速度大大提升。

为什么数组的长度是2的n次方呢?除了上面我所说的优点外,h&(length-1)运算等价于对length的取模运算,也就是h%length,&运算比%运算要高效得多。而且当数组的长度设为2的n次方时,不同的key酸的index相同的几率较小,那么数据在数组上分布就比较均匀,也就是发生哈希碰撞几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率就较高了。

在上篇博文中,我们上篇博文给出的HashMap的put(K key,V value)的源码,我们知道,当一个程序试图将一个key-value对放入HashMap中,首先根据key值计算出该元素要存储的位置

程序会首先计算该key的hashCode值(即h=key.hashCode()),然后对该哈希码再进行高位运算(也称为再哈希,即hash=h^(h>>>16)),然后把得到的哈希值(hash)和(数组长度-1)进行按位与(index=hash&(length-1))操作,最后得到要存储元素的数组下标(index)。

得到元素要存储的位置后,接下来就是把key—value对放入操作了,如果在Node[] table数组中对应的下标的链表中没有链表节点,那么久直接把包含<key,value>的节点放入该位置。如果该下标的链表已经有节点了,就对链表进行遍历,判断是否有和key值相同的节点,如果有的话,则直接用要存入的value替换原有的value。如果该链表中没有与key值相同的节点,则创建节点放入值,并把该节点插入到链表表头(头插法)。

HashMap的get(Object key)方法

上面说完HashMap的put的方法,现在再说说其get(Object key)方法,先看源码

public V get(Object key){
  if(key == null){
     return getForNullKey();
  }
  int hash = hash(key.hashCode());
  for(Node<K,V> e = table[indexFor(hash,table.length)];e!=null;e=e.next){
  Object K;
  if(e.hash == hash && ((k =e.key) ==key || key.equals(k))
    return e.value;
  }
  return null;
}

HashMap的get方法,是首先通过key的两次hash后的值与数组的长度-1进行与操作,定位到数组的某个位置,然后对该列的链表进行遍历,一般情况下,hashMap的这种查找时非常快的,hash值相同的过多,就会造成链表中的元素很多,而链表中的数据查找时通过遍历链表完成的,这可能会影响查询的速度。有一点需要特别注意的是:当查询完成后返回为null时,你不能判断它是没有找到对应的元素,还是在hashMap中存在一个value为null的元素,因为hashMap允许value为null

HashMap的扩容机制

在hashMap中,当节点的个数超过数组大小*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组的大小为16,那么当HashMap中节点超过16*0.75=12时,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,并把各个元素放进去,这时一个非常耗性能的操作。

多线程下HashMap出现的问题

1、多线程环境下hashMap进行put操作后,get操作导致死循环,导致cpu100%的现象。主要原因是多线程环境下,hashMap同时进行put操作时,如果同时出发了rehash操作,会导致扩容后的hashMap中的链表中出现循环节点,进而使得后面get的时候,会出现死循环。

2、多行程环境下hashMap进行put操作,导致元素丢失,也是发生在多线程对hashMap扩容时。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值