HashMap特性、原理及算法实现的一些思考

1、HashMap 一些特性:

  • 存储的是 <key, value> 形式的键值对;
  • 允许 key值 或 value值 为 null;
  • HashMap 是非 synchronized;
  • HashMap 很快
  • 哈希表的主干是数组,数组中的元素是链表,在 JDK8 中如果同一 hash 组成的链表元素大于等于 8 时,此数组元素将被调整成一颗红黑树。

 

2、HashMap 的工作原理:

        HashMap 是基于 hashing 的原理(不管后面进行什么操作都是要先对键值进行 hash 处理),我们 put(key, value) 来存储数据到 HashMap 中,使用 get(key) 从 HashMap 中获取数据,使用 resize() 来扩容。

        我们在使用 put() 方法传递键值对对象时,首先对键值调用 hash() 方法,得到 hash值, 利用此 hash 值查找此次传入键值对数据在数组中的存储位置。

       我们使用 get() 方法从 HashMap 中取值,它同样先是对键值调用 hash() 方法求得 hash 值,通过运算求得其在数组中的位置。

       默认的负载因子是 0.75,当 HashMap 的大小超过了 当前容量* 负载因子时, 它会进行 resize() 操作完成扩容,扩容后容量是之前的两倍,同时将之前的对象重新放入到新的 HashMap 数组中。

 

3、引申出的一些问题:

(1)当使用 put() 方法时,两个对象计算出的 hash 值 相同会发生什么?

       当两个对象计算出的 hash值 相同时,他们在数组中的位置就是一样的,一般来说就会发生碰撞,但是 HashMap 中的数组中的每个元素都使用链表的方式存储对象,这时会将新传入的键值对存入到对应位置的链表中。

(2)如果两个键计算出的 hash 值,你如何获取值对象?

       首先通过对键值调用 hash() 方法获得其对应的 hash 值,找到在数组中的位置,然后判断链接第一个节点是否满足,满足则直接返回值对象;不满足的话则继续遍历链表,直到找到链表中的键值对中的键值与传入键值相等为止,后返回对应的值对象。

(3)如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

       默认的负载因子是 0.75,当 HashMap 的大小超过了 当前容量* 负载因子时, 它会进行 resize() 操作完成扩容,扩容后容量是之前的两倍,同时将之前的对象重新放入到新的 HashMap 数组中,这个过程叫作 rehashing,因为它调用了 hash() 方法寻找了其新的数组位置。

 

4、扩展一下:

       JDK7 将新的元素插入到链表的头部,因为他们认为新插入的元素被用到机会比较大,放在头部便于查询,避免了尾部遍历。

       但是其在多线程的情况下会出现死循环,因为在扩容的时候,采用头部插入的方式可能会让之前存储在链表中的元素的次序会反过来,多线程的时候就会形成环形链表,出现死循环。

       JDK8 对这个对于新对象插入链表做了调整,采用尾部插入,有效的改进了这种情况。而且多线程的情况下一般不用 HashMap,而是要使用 ConcurrentHashMap。

 

5、思考:HashMap 初始长度,这样设计的目的?

        HaspMap 的默认初始长度是16,并且每次扩展长度或者手动初始化时,长度必须是2 的次幂。之所以是 16,是为了服务于从 Key 值映射到 index 的 hash 算法。

tab[i = (n - 1) & hash]

       n 是 HashMap 的容量,以 n = 16为例,与操作保证了获取 hash 的低 4 位,即为此 key 值对应的数组位置 index。

       同时为了实现一个尽量分布均匀的 hash() 函数,利用的是 Key 值的 HashCode 来做某种运算。

 

      JDK8 中使用如下方式来实现。

(h = key.hashCode()) ^ (h >>> 16)

        hashCode 高16位保持不变,低 16 位与高 16 位进行异或,这样可以保证数组比较小时候高低位都能参与到运算中, JDK7 有类似的四次扰动计算,JDK8 只有这么一次,可能是从速度或者效率方面的考虑。

例:

  •         假设现在 hashCode 只有 8 位
  •         采用 (h = key.hashCode()) ^ (h >>> 4) 来实现 hash()
  •         计算在数组中的位置 index = tab[i = (n - 1) & hash] (n=16)
  •         如果此时有低四位相同,高四位不同的两个 hashCode

        0100 1000 无符号右移 4 位: 0000 0100 计算得到 hash = 0100 1100 index = 12

        0010 1000 无符号右移 4 位: 0000 0010 计算得到 hash = 0010 1010 index = 10

        通过上述方法就可以获得不同的 index 值,让高低位都参与运算有利于对象在数组中均匀分布,如果直接使用原始的 hashCode 值或者做取模运算则会等到两个一样的值,使得分布不均匀。同理 hashCode 为 32 位也是一样的道理。

 

6、JDK7 与 JDK8 中 HashMap 的区别:

  • JDK7 之前的 HashMap 又叫散列链表:基于一个数组及多个链表的形式存储,hash 值冲突的时候,就将对应节点放到链表中存储。
  • JDK8 中,当同一个 hash (在 Table 上的元素)的链表节点数大于等于8时,将不在以单链表的形式存储了,会被调整成一颗红黑树,这就是 JDK8 与 JDK7 中HashMap 实现的最大区别。
  • JDK8 中,对于同一 hash 组成的链表的元素插入是在链表尾部插入的,JDK7 及以前版本是在链表头部插入的

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值