- 在 JDK 1.7 中,HashMap 采用数组+链表的方式实现
- 在 JDK 1.8 中,HashMap 采用数组+链表 / 红黑树实现
Entry[] table=new Entry[capacity];
基础变量
- DEFAULT_INITIAL_CAPACITY(Table 数组的初始大小):1 << 4,这里需要是 2 的 n 次方,数组扩容的时候也是每次扩容 2 倍,根本原因是在计算哈希散列时,会使用下面的公式
- hashCode 的高 16 位和低 16 位进行异或运算,避免只使用低位的情况
- n - 1 & hashCode 可以将哈希散列均匀散布到 Table 数组中
-
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } tab[i = (n - 1) & hash]
- MAXIMUM_CAPACITY(Table 数组的最大长度):1<<30 = 1073741824
- DEFAULT_LOAD_FACTOR(负载因子):默认值为 0.75。当元素的总个数 > 当前数组的长度 * 负载因子时,数组会扩容为原来的两倍
- TREEIFY_THRESHOLD(树化阈值):默认值为 8,当一个 Table 节点下的 Entry 个数大于 8 时,会将链表转换成为红黑树
- UNTREEIFY_THRESHOLD(链化阈值):默认值为 6,当一个 Table 节点下的 Entry 个数小于 6 时,会将红黑树转化成为链表
- MIN_TREEIFY_CAPACITY(最小树化阈值)= 64,当 Table 数组元素超过该值,才会进行树化(防止前期阶段频繁扩容和树化过程冲突)
问题一:Table 数组可以替换为 LinkedList 或者 ArrayList 吗?
- 由于通过哈希散列值可以快速定位数组元素,而 LinkedList 内部通过链表维护,效率较低
- ArrayList 底层采用数组实现,但是扩容时会扩展 1.5 倍,而 Table 数组为了哈希散列更均匀,需要扩容 2 倍
问题二:简单说一下 HashMap 的 put 元素的过程吗?
- 通过对 key 做 hash 运算,计算 index
- 如果没碰撞,进行扩容检测,并直接放到 bucket ⾥
- 如果碰撞了,以链表的形式存在 buckets 后,如果碰撞导致链表过⻓(⼤于等于TREEIFY_THRESHOLD),就把链表转换成红⿊树
- 如果节点已经存在就替换原有的 value
问题三:简单说一下 HashMap 的 get 元素的过程吗?
- 通过对 key 做 hash 运算,计算 index
- 如果在 bucket ⾥的第⼀个节点⾥直接命中,则直接返回
- 如果有冲突,则通过 key 去查找对应的 Entry,若为树,查找效率为 O(logn),若为链表,查找效率为 O(n)
String.hashcode()
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
问题四:知道 JDK1.8 中 HashMap 改了什么吗?
- 由数组+链表的结构改为数组+链表+红⿊树
- 优化了⾼位运算的hash算法
注意事项
- HashMap 扩容是一个特别消耗内存的操作,所以尽量在 HashMap 初始化的时候预估一个大致的数值,避免 Map 进行频繁的扩容
- 负载因子是可以修改的,也可以大于 1,但是建议不要轻易修改,除非情况非常特殊
- HashMap 是线程不安全的,不要在并发的环境中同时操作 HashMap,建议使用ConcurrentHashMap