书接上文,HashMap源码分析上文书对hashmap的源码做了一个简单的解析,这回我们来说一下HashMap的数据结构问题。
上文中说到HashMap的基础结构一个初始容量为16的数组,加载因子是0.75,也就是容量使用到12的时候会触发扩容机制,扩容后的容量为原来的2倍。
但这并不是说超过12个元素就会扩容,其实这里容量为16的数组并不只是一个简单的数组结构,而是数组在链表的结构。类似于下面图中这样的一个结构:
数组中的每个索引中包含的元素可能是多个,也就是可能>=0个元素。那么这个索引的值是怎么计算的呢?我们在源码可以看到
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
其中hash函数的代码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
即通过上面做对计算出key的hash值,然后我们在putVal()中可以看到如下的代码:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
..........
其中关键代码是这句:
i = (n - 1) & hash
即,是通过key的hash值和容器的大小减1,两者进行与运算,获取容器数组下标。这里使用与运算,其实蕴含了一个隐藏条件,即数组的大小n,必须是2的n次方,否则,计算出来的下标值i是无法覆盖这个范围[0, n-1]的。这就是为什么每次扩容都是*2的原因。
而如果这个链表可以无限延长,那么毫无疑问会造成查询数据的性能问题,于是JDK1.8引入的红黑树,上文中提到的由链表转红黑树的阈值是8,也就是如果链表的长度到8就会转变成树结构。以此来解决链表过长而引发的性能问题。红黑树的存储结构大概如下:
这里顺便推荐一个数据结构可视化的网站https://www.cs.usfca.edu/~galles/visualization,可以直观的了解红黑树和其它数据结构,也可以对比其它树结构,应该就能明白为何HashMap选用红黑树而不是其它的树型结构。这里就不再赘述。