这里只讨论Java8中我对HashMap源码的理解,因为Java8对HashMap做了优化,可能实现已经跟以前版本很大不同了。但是我不想贴源码,只写容易理解错和忽略的东西。
Java面试必问的HashMap底层实现原理,你可能仅仅知道,HashMap底层是数组+链表,可以在构造方法中初始化数组长度,其通过object的hashcode方法模数组长度确定元素在数组中的位置,当链表长度达到8时转为红黑树,并且HashMap是线程不安全的。
可能你只知道上面,但这是远远不够的,你知道HashMap为什么线程不安全吗?更致命的是,上面这些理解部分是错误的。

构造时并不会初始化数组
HashMap内部使用一个数组来存放元素,变量名为table,我们称为“桶数组”,其中数组中每一个空位称之为“桶”。当你使用HashMap构造方法语句Map m = new HashMap<>(10);来初始化一个HashMap时,系统底层并不会给你初始化一个容量为10的桶数组,而是延迟到第一次put存储键值对的时候再做resize扩容初始化桶数组。
初始化桶数组时并不是你指定的初始容量
我们知道,HashMap中桶数组的长度始终都为2的n次幂,每次扩容的时候都是翻倍扩容。那我执行Map m = new HashMap<>(10);最后也是2的n次幂吗?是的没错,HashMap中会找到比你的初始容量大的最近的2的n次幂的值,如:你传的是10,那么桶数组长度会为16。你传的是18,那么桶数组长度为32。

hash散列并不是简单的使用Object的hashcode()
HashMap中的hash散列并不是简单的直接使用了hashcode()方法,而是加入了“扰动函数”,源码如下,使用了h >>> 16使得高16位也参与了散列的过程。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
求模并不是简单的使用求模运算%
桶位置的计算,源码中使用的是(n - 1) & hash,n表示桶数组长度,我们知道n桶数组长度使用是2的n次幂,故hash和(n - 1)做“与”操作,相当于取模了,使用位运算,提高了效率,这也是为什么HashMap的桶数组长度需要是2的n次幂的原因。如果key为空,其hash值设置为0。
链表达到8就一定会转为红黑树(树化)
并不是,链表达到8时,会调用树化方法,方法中还判断了桶数组是否大于等于MIN_TREEIFY_CAPACITY默认64。若小于64,不树化,而是resize扩容数组。当然,除了树化,还有反树化,resize扩容时会判断,当红黑树的元素小于等于6时,红黑树转为链表。
新元素是插入链表尾部的
没错,Java8中HashMap是采用的尾插法,而Java8以前的版本使用的是头插法,主要是头插法在并发可能导致链表成环,造成读取时死循环。同时,Java8是在插入数据后判断阈值再扩容的,而以前是先扩容再插入。
HashMap怎么扩容的
HashMap扩容时,新建了一个2倍大的桶数组,按顺序遍历旧桶,如果某个桶上只有一个Node,则直接计算该Node的新桶的位置。如果某个桶上是一个链表,则里面的元素我们知道其要么是还在原来的位置,要么新桶位置是原来的位置移动oldCap旧容量大小,HashMap中使用了(e.hash & oldCap) == 0来判定是原来的位置还是新位置。举个例子,如果原来桶大小是16,现在两个元素,一个hash值是1,一个是17,他们都会在桶位置为1的链表上。但此时扩容了,那么hash为17的应该就会跑到1+16=17的新桶位置。扩容时,如果是链表,会使用头尾指针来连接新链表,同时新桶和旧桶使用了hiHead、loHead两个指针,即:(loHead、loTail)、(hiHead、hiTail)
以上。