目录
HashMap容器简介
HashMap以K/V形式来存储数据,基于哈希表结构,本质上是一个数组+链表的结构,提供了高效率的添加和检索。影响HashMap性能的主要有两个因素,一个是桶的数量,另外一个就是加载因子,桶数*加载因子就是HashMap扩容的临界值。如果扩容临界值设置过小,实际存储数据又过多,扩容次数就会很频繁,从时间成本上就会影响性能。相反如果临界值设置过大,get和迭代操作性能就会降低,这是由空间成本引起的。 与Hashtable相比,HashMap允许使用 null 值和 null 键,除了非同步和允许使用 null 之外,它与 Hashtable 大致相同,此外HashMap的迭代器也是快速失败的,因为迭代过程中不会检测对集体结构上的修改(比如put一个新k/v,或remove已有值,但不包括替换),从而会出并发修改异常ConcurrentModificationException。
HashMap源码及数据结构分析(版本JDK1.8.0_131)
初始化:基本状态及构造函数部分源码
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,默认容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
transient Node[] table; //Map.Entry数组(桶数组)
transient int modCount; //结构被修改的次数
int threshold; //下次扩容临界值
final float loadFactor; //加载因子(不允许修改)
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); //首次扩容临界值=大于给定cap的第一个满足2的N次幂的值(如15则首次扩容为临界值为2的4次方=16)
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
从源码可以看出,在JDK1.8中HashMap默认的初始容量仍然是16,加载因子是0.75,扩容临界值threshold仍然等于【桶数*加载因子 =(int)8*0.75 = 6】(虽然在1.8的构造器中把threshold设置成了取大于capacity的第一个等于2的N次幂【8】,但第一次put后又会把threshold变成桶数*加载因子,所以目前来看构造器中的设置并没有意义)。此外在JDK1.8中,new HashMap<>()时不会创建Node
put()/get()核心源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //构造时没有创建table,所以首次扩容发生在第一次put。
if ((p = tab[i = (n - 1) & hash]) == null) //校验table[i]位置是否被占用,对于key==null的键,其hash==0,永远处于第一个元素)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) { //迭代,如果在迭代过程中发现相同Node,则记录该Node。否则把新的Node添加到链接末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //链表长度达到8个即转为红黑树结构
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key (如果存在相同Node,则进行值的替换)
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize(); //重调Map
afterNodeInsertion(evict);
return null;
}
/**
* 重调map
*/
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //newCap调整为oldCap的两倍
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold //大于=16时,调整为oldThr两倍,其实等价于newCap * 加载因子,比如newThr(8*0.75) = oldThr(4*0.75)*2 = 3 * 2
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap]; //创建新Node数组
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { //复制元素,并删除旧Node数组元素
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order 保证原链表顺序,由于扩容后table长度会发生变化,所以需要重新计算每个Entry的存储位置, 这里拆分Entry链。
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
从源码可以看出HashMap的存储结构与存储过程:HashMap内部维护了一个存储数据的Entry数组(并保证该数组不管是初始化还是扩容后其大小永远是),Entry本质上是一个单向链表,HashMap采用该链表来解决key冲突的情况,key为null的键值对永远都放在以table[0]为头结点的链表中。当put一个key-value对时,首先通过hash(key)计算得到key的hash值,然后结合数组长度通过计算(table.length - 1) & hash得到存储位置table[n](使得所有二进制位都为1),如果table[n]位置未被占用则创建一个Entry并插入到位置n;否则产生碰撞,迭代Entry链依次比较新Key与每个Entry.K,如果key.equals(Entry.K)==true则替换旧的value,否则创建新Entry并插入到链表的末尾(1.7中在头部),默认情况下当HashMap大小达到容量的75%时,会执行扩容操作创建一个新的Entry数组,长度是原来的2倍,在1.8中当某个Entry链长度>= 8 时,还会把该Entry链转换为红黑树结构。在扩容过程中由于Node
下面代码虽然简单但却非常全面的测试了map初始化、碰撞及扩容过程
public static void main(String[] args) throws Exception {
HashMap map = new HashMap<>(1);
map.put(1, 1);
map.put(3, 3);
}
从源码角度分析代码:
(1) new HashMap<>(1):最终结果capacity == 1、threshold == 2的0次 == 1、Node
(2) map.put(1,1);这里执行了两次resize()操作。
(3) map.put(3,3) 同样由于(tab.length - 1 & 3) == 1 & 3 == 1, 所以Entry<3,3>同样应处于tab[1]位于并且作为Entry<1,1>的next元素而存在。但此时由于++size(++1)又大于了threshold(1),将再次执行resize(),最终capacity == 4、threshold==-3,由于Entry[] table的容量发生了变化,所以此时需要迭代每个Entry链,对Entry链中的每个元素进行重新计算,得出新的存储位置。所以这里tab[1]处的Entry链(Entry<1,1>-->Entry<3,3>)将会被拆分,因为(tab.length - 1) & 3 = 3 & 3 = 3,不再是1,所以最终会把Entry<3,3>存储在tab[3]。而Entry<1,1>仍然处于tab[1]处。最终结果及对应变化可以用下图表示
数据结构
上方的源码其实已经说明了HashMap底层的数据结构,图解的话大致如下
注意问题及性能优化
HashMap并不是线程安全的,如果需要线程安全的话可以考虑通过Collections.synchronizeMap(hmap)来包装,但这种方式本质上和HashTable没什么区别,都是以map本身为锁对象,效率并不高。并发情况下应该使用ConcurrentHashMap
HashMap扩容时,由于Node
不考虑冲突的情况下,HashMap的复杂度为O(1),不管数据量多大,一次计算就可以找到目标;当然实际应用中随着Key的增加,冲突的可能性越大, 如果请求大量key不同,但是hashCode相同的数据甚至可以造成Hash攻击,让HashMap不断发生碰撞,硬生生的变成一个单链表,这样put/get性能就从O(1)变成了O(N)。从这点出发应尽量减少碰撞的机率,使用比较高率的hashCode,比如可以采用Integer、String等final变量作为Key,这种类型的hash值产生冲突的可能性很少,比如1的hashCode就是1,2的就是2。
另外一种常见的问题就是HashMap的扩容时死循环问题
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry e = src[j];
if (e != null) {
src[j] = null;
do {
Entry next = e.next; //若线程A执行此行被挂起,线程B整个更新链表。线程A继续运行,则很可能产生死循环或者put丢失。
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
死循环问题在1.8中已经被解决,1.8中在扩容时声明了两对指针,维护两个链表,依次在末端添加新的元素,在多线程操作的情况下,不会出现交叉的情况,顶多也就是每个线程重复同样操作。
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
HashMap迭代过程中不会检测对map结构的修改,所以并发访问情况下(或单线程下在迭代中修改)很容易抛出ConcurrentModificationException。