前言
本文分析的代码对应的JDK版本为1.8,采用的IDE环境为IntellJ IDEA,注意本文不做细致分析,所以不会对源码细节做过多说明,而是指出使用HashMap前需要了解哪些方面的内容,可能会发生什么问题,细节可以自己参考JDK源码。
基本数据结构
数组和链表的结合,
- 数组用来存放每个链表的头节点(数组的元素俗称桶(Bucket));
- 链表用来存放hash值相同的节点;(存放位置由key的hashCode值决定;
数组说明
- 长度大小需为2^n^(n为整数,n>= 0)(默认为16),至于为什么有这个要求,后面说明;
- 有一个关联的加载因子loadFactor(默认为0.75),默认的可用的桶为12个,由一个threshold变量记录;长度大于数组length * loadFactor 会重新resize;
元素如何添加进HashMap的
添加元素,采用put方法,put方法实现如下:
1 | public V put(K key, V value) { |
调用之前,先通过hash()方法获取hash值,hash函数如下:
1 | static final int hash(Object key) { |
这里是HashMap支持Key为空的关键,Key值为空时,其对应的hashCode被置为0,也就放在了数组的第一个元素。
内部调用putVal方法:
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
依据putVal()函数的源码,来说明为什么需要2^n^的长度,看putVal()代码片段
1 | if ((p = tab[i = (n - 1) & hash]) == null) |
以默认的数组容量16为例,n-1=15,对应二进制为00001111,那么:
- hash>15,位运算后结果为hash和(n-1)后面三维位运算的结果其实就是%运算
- hash<=15,位运算结果为hash本身。
这应该就是数组长度要求2^n^的原因,方便进行位运算(用空间换时间)
为什么说HashMap不是线程安全的
因为多线程都rehash时,可能会导致循环链表的形成,可能形成条件竞争。
为什么推荐使用String、Integer这样的类作为Key
因为这些类的对象是不可变的,也是final的,并且都自己实现了hashCode和equals方法,这就保证了同一个对象其HashCode不会发生改变,减少了hash碰撞发生的几率,可以提高HashMap的性能。
hash碰撞
当向HashMap中添加
- 两次的hashCode相同,但K值不相等(调用equals()判断),就将后加入的
添加到先加入的 的尾部; - 两次hashCode相同,且K值相等(调用equals()判断),后加入的就取而代之。
顺便谈谈LinkedHashMap
LinkedHashMap其实就是HashMap和双向链表的综合体,其中:
- 通过继承HashMap,保留了HashMap的特点
- 通过双向链表,实现了功能的扩展(解决了HashMap不能保证有序的问题)
- 可以记录插入顺序(默认)
- 可以记录访问顺序, 重写
removeEldestEntry(Map.Entry<K,V> eldest)
方法还可以很好的实现LRU算法
有序性原理
在调用put和get方法时会分别调用afterNodeInsertion
afterNodeAccess
方法,这两个方法起着记录相关顺序的作用(插入顺序和访问顺序),本质上就是双向链表的操作。然后,就是通过重写迭代器将LinkedHashMap中的所有Entry按照记录的顺序迭代输出,所以迭代器的重写是关键