HashMap详解

一、 HashMap、Hashtable和ConcurrentHashMap的区别

1. HashMap的特性:

(1)存储键值对,实现快速存取数据;
(2)允许键/值为null,但不允许重复的键;
(3)非同步synchronized(比同步快),线程不安全;
注:让HashMap同步: Map m = Collections.synchronizeMap(hashMap);
(4)实现Map接口,对键值对进行映射,不保证有序(比如插入的顺序)
注:Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。
(5)HashMap默认的容量大小是16;增加容量时,每次将容量变为“原始容量x2”
(6)HashMap添加元素时,是使用自定义的哈希算法;

2. Hashtable:

(1)不存储键值对,仅存储对象;
(2)不允许键/值为null;
(3)线程安全(速度慢),采用synchronize关键字加锁原理(几乎在每个方法上都加锁),;
(4)实现了Set接口,不允许集合中有重复的值。注:将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,
比较对象的值是否相等,以确保set中没有储存相等的对象。hashCode()可能相同,用equals()判断对象的相等性。
(5)Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 + 1”;
(6)Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。

3. ConcurrentHashMap

(1)Java并发包java.util.concurrent中的一个线程安全且高效的HashMap实现
(2)不允许键/值为null;
(3)线程安全:在JDK1.7中采用“分段锁”的方式,1.8中直接采用了CAS(无锁算法)+ synchronized。

二、HashMap基本概念

Entry:HashMap是一个用于存储Key-Value键值对的集合,每一个键值对叫做Entry,这些Entry分散存储在一个数组当中。
hashMap是在bucket中储存键对象和值对象,作为Map.Entry
bucket:HashMap初始化时,创建一个长度为capacity的Entry数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,
每个bucket都有其指定索引,系统可以根据其索引快速访问该bucket里存储的元素。
loadFactor:负载因子。默认值DEFAULT_LOAD_FACTOR = 0.75f;
capacity:容量,指的是数组的长度
threshold:阈值=capacity*loadFactor。超过阈值则扩容为原来的两倍。
size:HashMap的大小,它是HashMap保存的键值对的数量。

三、HashMap的原理

HashMap是基于hashing的原理,底层使用哈希表结构(数组+链表)实现。使用put(key,value)存储对象,使用get(key)获取对象。
理解为,数组中存储的是一个Entry,并且作为链表头结点,头结点之后的每个节点中储存键值对对象Entry。

1. put():

给put()方法传递键和值时,先对键调用hashCode()方法计算hash从而得到bucket位置,进一步存储,
HashMap会根据当前bucket的占用情况自动调整容量(超过负载因子Load Facotr则resize为原来的2倍)。
扩容扩的是数组的容量,发生碰撞后当链表长度到达8后,链表上升为红黑树,提高速度。

2. get():

根据键key的hashcode找到bucket位置,然后遍历链表节点,调用equals(用来获取值对象)方法确定键值对,找到要找的值对象。

3. hash函数怎么实现

a.对key的hashCode做hash操作(高16bit不变,低16bit和高16bit做了一个异或)
b.计算下标(n-1) & hash,从而获得buckets的位置 //h & (length-1)

4. 其他hash的实现方式?

数字分析法、平方取中法、分段叠加法、 除留余数法、 伪随机数法。

四、HashCode相同发生碰撞

1. HashMap中处理冲突的方法实际就是链地址法,内部数据结构是数组+单链表,当发生碰撞了,对象将会储存在链表的下一个节点中。

其他解决hash冲突办法:开放定址法、链地址法、再哈希法。

2. hashcode相同,也就是bucket位置相同,因此发生“碰撞”。因为HashMap使用链表存储对象,这个Entry会存储在链表中。

根据hashcode来划分的数组,如果数组的坐标相同,则进入链表这个数据结构中,jdk1.7及以前为头插法,jdk1.8之后是尾插法,
在jdk1.8之后,当链表长度到达8的时候,jdk1.8上升为红黑树。存的时候按照上面的方式存,取的时候根据equals确定值对象。

五、超过负载因子则扩容rehashing

  1. 扩容rehashing:它重建内部数据结构,并调用hash方法找到新的bucket位置。大致分两步:
      a.扩容:容量扩充为原来的两倍(2 * table.length);
      b.移动:对每个节点重新计算哈希值,重新计算每个元素在数组中的位置,将原来的元素移动到新的哈希表中。
  2. 默认的负载因子大小为0.75,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样开始扩容,
    创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。

六、Resize存在线程安全问题

  1. HashMap在resize重新调整大小时,多个线程并发操作则存在条件竞争,会导致死锁死循环。考虑用ConcurrentHashMap。
    因为如果两个线程都发现HashMap需要重新调整大小了,会同时试着调整大小。
  2. 在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,
    HashMap并不会将元素放在链表的尾部,而是放在头部,避免尾部遍历,原数组[j]位置上的桶移到了新数组[j+原数组长度]。

七、HashMap使用类型的元素作为Key

  1. 使用String、Interger这样的wrapper类作为键,或者使用不可变的、声明作final的对象;
    因为这些类是Immutable不可变类,并且很规范的覆写了hashCode()以及equals()方法,将会减少碰撞的发生,提高效率。
    如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

  2. 不可变类天生是线程安全的,不可变性使得能够缓存不同键的hashcode,避免重复计算等等。

  3. 实现一个自定义的class作为HashMap的key,覆写hashCode以及equals方法应该遵循的原则。

八、总结

1.常见问题:集合类、数据结构、线程安全、解决碰撞方法、hashing概念和方法、equals()和hashCode()的应用、不可变对象的好处

  1. HashMap基于hashing原理,通过put()和get()方法储存和获取对象。
    当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。
    当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
    HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。

https://blog.csdn.net/weixin_42636552/article/details/82016183
https://blog.csdn.net/u012512634/article/details/72735183
https://blog.csdn.net/zaimeiyeshicengjing/article/details/81589953
https://blog.csdn.net/aura521521/article/details/78462459

推荐阅读更多精彩内容