HashMap内部结构
jdk8以前:数组+链表
jdk8以后:数组+链表+红黑树 (当链表长度到 8 且table的大小 >= 64时,转化为红黑树,)
数组结构:
transient Node<K,V>[] table;
链表结构:
HashMap<String, Double> hashmap = new HashMap<>();
map.put("k1",0.1);
map.put("k2",0.2);
map.put("k3",0.3);
map.put("k4",0.4);
构造函数:
向hashmap中添加第一个元素:
然后进入 hash 函数中
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
发现它并不仅仅是取 key 对应的hashCode
(h = key.hashCode()) ^ (h >>> 16) 按位异或,把hashCode算术右移16位
在这里我简单的理解是为了使分布更加均匀,尽量让不同的 key 对应不同的 hash 值,避免哈希碰撞。
然后就进入了最核心的函数 putVal 函数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //定义了一些辅助变量
System.out.println(threshold);
if ((tab = table) == null || (n = tab.length) == 0) //table就是上面说的HashMap的数组结构 Node[]
//此时进行第一次扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> 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<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal方法执行流程图:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
面试题:如果在new HashMap的时候,传入的size是300,则添加300个元素的时候,HashMap总共经过几次扩容?
答案是一次都没有。
分析过上面的源码,那么解决这个问题就很简单了,首先默认容量传入的是300,那么HashMap会先被处理成2的倍数,也就是2的9次幂 512,那么当前HashMap的实际容量其实是 512。
根据512 * 0.75 = 384。也就是说,下次扩容应该是当容量达到384时。
所以默认给300个size时是不需要扩容的。
根据阿里巴巴Java开发手册,集合在初始化时,最好指定好集合的初始值大小
面试题:
说⼀下HashMap的Put⽅法
先说HashMap的Put⽅法的⼤体流程:
- 根据Key通过哈希算法与与运算得出数组下标
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是 Node对象)并放⼊该位置
- 如果数组下标位置元素不为空,则要分情况讨论
a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对 象,并使⽤头插法添加到当前位置的链表中
b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过 程中会判断红⿊树中是否存在当前key,如果存在则更新value
ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插 ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊ 到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成红⿊树
iii. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就 扩容,如果不需要就结束PUT⽅法
Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?
- 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询 整体效率
- 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断 链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
- 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就 是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希算法, 节省CPU资源