一、结构
![58d45495d80a139e50ae69bb957d79c4.png](https://i-blog.csdnimg.cn/blog_migrate/457e5077ce2884b45db03d23fa892cdf.jpeg)
如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数:
- 容量
- 负载因子
容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size > 16*0.75 时就会发生扩容(容量和负载因子都可以自由调整,无论是自动扩展还是手动初始化时,必须是2的幂)。
二、方法
2.1、put 方法
首先会将传入的 Key 做 hash 运算计算出 hashcode,然后根据数组长度取模计算出在数组中的 index 下标。
由于在计算中位运算比取模运算效率高的多且Hash算法均匀分布原则,所以 HashMap 规定数组的长度为 2^n 。这样用 2^n - 1 做位运算与取模效果一致,并且效率还要高出许多index = HashCode(Key) & (Length - 1)。
下面我们以值为“book”的Key来演示整个过程:
1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。
由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 table[index]处形成环形链表,采用头插法(不是插入链表的尾部,而是头部,因为:后插入的Entry被查找的可能性更大)将数据插入到链表中。
2.2、get 方法
get 和 put 类似,也是将传入的 Key 计算出 index ,如果该位置上是一个链表就需要遍历整个链表,通过 key.equals(k) 来找到对应的元素。
遍历方式
Iterator> entryIterator = map.entrySet().iterator(); while (entryIterator.hasNext()) { Map.Entry next = entryIterator.next(); System.out.println("key=" + next.getKey() + " value=" + next.getValue());}
Iterator iterator = map.keySet().iterator(); while (iterator.hasNext()){ String key = iterator.next(); System.out.println("key=" + key + " value=" + map.get(key)); }
强烈建议使用第一种 EntrySet 进行遍历。
第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。
三、高并发下的HashMap
在并发环境下使用 HashMap 容易出现死循环。
HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。扩容的影响因子就是Capacity(容量)、LoadFactor(加载因子),当 HashMap 的 size > 16*0.75 时就会发生扩容。
3.1、扩容
3.1.1、创建一个新的Entry数组
长度时原来的两倍。
3.1.2、ReHash方法
/** * Transfers all entries from current table to newTable. */void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry e : table) { while(null != e) { Entry next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } }}
并发场景发生扩容,调用 Resize() 方法里的 Rehash() 时,容易出现环形链表。这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标时就会出现死循环,避免这种情况,通常采用ConcurrentHashMap。
![7247bfd1f40bccfed8f1fc2c26af92cc.png](https://i-blog.csdnimg.cn/blog_migrate/9c197ca16e302ba5c7a77f569d4f02bb.jpeg)
所以 HashMap 只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。
四、优化
在 JDK1.8 中对 HashMap 进行了优化:
当 hash 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为红黑树。
假设 hash 冲突非常严重,一个数组后面接了很长的链表,此时时间复杂度就是 O(n) 。如果是红黑树,时间复杂度就是 O(logn) ,大大提高了查询效率。
![2f60842b53961fe2aaaac3265095bfa1.png](https://i-blog.csdnimg.cn/blog_migrate/794e1a2d3e13f89be64d2cbd5f62ada4.jpeg)