面试官:了解HashMap实现原理吗?
先来的图吧 顺便加深集合框架知识体系 了解现在问在哪里
问:说下Hashmap的初始化过程
几个构造函数主要围绕两个参数进行
1 负载因子 控制扩容 缺省值0.75
2 容量:
a 构造函数的时候为了省空间 会先用threshold存一下 put的时候再重新设置threshold 缺省值16
//构造方法
public HashMap(int initialCapacity, float loadFactor) {
...
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//存放值时候初始化并resize
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
....
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
....
}
//resize 重新设置threshold 和cap
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//这里
int newCap, newThr = 0;
if (oldCap > 0) {
.....
}
else if (oldThr > 0)
newCap = oldThr; //这里
else {
newCap = DEFAULT_INITIAL_CAPACITY;//这里
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//这里
}
b 初始化容量的时候 会自动调整成2的n次方 为了计算散列表位置需要使用位与2的n次幂-1(1111..)运算 保证每个下标都能命中。
问:了解hash怎么设计的吗 为什么这样? 为什么要与?
a:1.8 取key的hashcode 高低16位进行异或 然后和数组与
b: 使用2的n次幂做掩码 所以hashcode 只会在有较小的集合上产生影响 扩展高位上影响 又为了减少系统消耗使用异或方式 简单有效的避免冲突! 而且hash集合一般已经分布比较均匀 况且还有树来处理大量冲突
c:map容量不会有那莫大 2的31次方那么大
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(n - 1) & hash
问:1.7 和1.8 hashmap的优化了什么 为什么
0:结构
冲突解决方案里面增加了红黑树
1:hash
1.7 4次扰动
1.8 因为有红黑树解决冲突问题 扰动一次
2:put
由头插改成尾插
1.7头插法,单链表 当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链
1.8尾插法 红黑树使用尾插法,能够避免出现逆序链表死循环的问题
3:resize
扩容rehash过程省去 使用原hash值计算 扩容后 掩码只比以前扩了一位1 (如 0000 1111-》0001 1111)
a在计算index的时候如果扩容未参与运算(判断方法 e.hash & oldCap 2的n次幂) 下标结果是一样的 index(原)=index(新)
b如果参与与运算 也相当与 加上高位的值 就是扩容的值 index(原) = index(新)+扩容值
扩容顺序 由先扩容再插入 改为先插入后扩容
a原因 没有了hash 添加之后再扩容更方便 不用预先hash 和equil判断
问:什么时候转红黑树?为什么? 冲突量降下来怎么办
a:在冲突必要严重的情况下 链表长度大于8 树形化结构还有个条件 map容量需要超过64
b:查找时间复杂度由O(n) 转为 O(logn)
hash设计合理的话 节点分布在hash桶中的频率遵循泊松分布 冲突8的情况很小
c:避免来回转换 数结构转链表阈值是6
问:hash冲突解决方式有哪些
1本文的链地址
2开放地址发 hash内加入一个探测量 冲突之后进行线性 二次 伪随机
3构造多个hash函数
问:put为什么改成尾插?除了尾插有没有其他解决方式?
a:头差法危害 引用倒转 多线程死循环
b1:环检测 差速遍历 有环肯定会相交
b2:借助额外缓存 每次判断已经遍历