【Java集合】4 HashMap

实现原理

Q:HashMap 的实现原理/底层数据结构?

HashMap 底层是数组 + 链表,JDK 1.8 后增加了红黑树:

  • 数组中存储着 Node 对象,本质是一个键值对,内部包含 key、value、hash、next 属性;

  • 有时两个 key 会定位到相同的位置,表示发生哈希冲突;解决方法是采用链地址法,在这个位置挂一个链表,放入冲突的键值对;

  • 当链表长度大于 8 时转换为红黑树,来提高 put、get 效率到 O(logN)。

    Q:为什么是8,不是16,32甚至是7 ?(为什么先用链表,再转红黑树?)

    因为经过计算,元素小于 8 时,链表可以保证查询性能,增删修改 Entry 引用即可,而红黑树需要左旋、右旋、变色等操作保持平衡,大于 8 时,链表查询性能下降,就需要转为红黑树来加快查询速度。

img

hash

Q:HashMap 的哈希函数如何设计?

哈希算法分三步:

// JDK 1.8
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(n - 1) & hash
  1. 取 key 的 hashCode 值;

  2. 高位运算:

    1.7 做了四次位移和异或,比较多余;

    1.8 进行了优化,改成一次,让其高 16 位异或低 16 位,使得后续寻址时,即使 length 比较小,也能保证高低位都参与哈希计算,减少哈希冲突,同时不会有太大的开销;

  3. 取模运算:因为 length 始终为 2 的 n 次方,所以 (length - 1) & hash 可以等同于对 length 取模,但性能更好。

Q:为什么这么设计?

  1. 尽可能降低哈希碰撞;
  2. 使用位运算可以提高性能。

Q:为什么不直接用 key 的 hashCode 值就行,还要高位和取模?

key 的 hashCode 值 是一个 int 型整数,范围在 -231到 231,很明显我们不可能建一个这么大的数组,所以需要取模;

Q:为什么不直接取 key 的 hashCode 值,而是高位后再进行取模?

高位运算可以使得后续寻址时,即使 length 比较小,也能保证高低位都参与哈希计算,减少哈希冲突;

put

Q:HashMap 的 put 方法的执行过程 / 数据插入原理?

  1. hash(key):计算 key 的 hash 值;
  2. 判断数组是否为空,为空调用 resize,进行初始化;
  3. (n - 1) & hash:确定数组下标 i;
  4. 判断 tab[i] 是否为空,为空直接插入;
  5. 不为空,表示发生哈希冲突,如果和首节点 hash、key 值相等,就覆盖 value;
  6. 若不等,判断是否为树节点,是就插入红黑树中;
  7. 不是就遍历链表,若 key 值存在,就覆盖 value,最后在尾部插入新节点,如果链表长度大于 8,就转换成红黑树;
  8. 插入成功后,如果 ++size > threshold,就要 resize,进行扩容。

get

Q:HashMap 的 get 方法的执行过程?

  1. hash(key):计算 key 的 hash 值;
  2. 判断数组以及 tab[(n - 1) & hash]) 是否为空,为空返回 null;
  3. 不为空时,如果和首节点 hash 值、key 值相等,返回首节点;
  4. 如果不止一个节点,判断是否为树节点,是就遍历红黑树取数,否就遍历链表取数;

注意不能根据 get 的返回值,判断 key 是否存在,要用 containsKey,

因为 get 返回空,可能是 key 不存在,也可能是 value 为空。

resize

Q:HashMap 的 resize 方法的执行过程?

  1. 判断 oldTable 是否为空,为空就进行初始化并返回;

  2. 不为空,则左移 1 位,扩容为原来(原数组长度)的两倍;

  3. 遍历 oldTable,

    JDK 1.7 要 rehash,用 (n - 1) & hash 重新确定每个元素的数组下标;

    JDK 1.8 采用更简单的判断逻辑,在原位置或原位置 + 旧容量;因为每次扩容都为原来的两倍,所以 n - 1 的二进制会比原来高位多一个 1,而 1 & 任何数都是该数本身,所以只需看原 hash 值该 bit 是 0 还是 1,0 在原位置,1 为原位置 + 旧容量。

    源码中用 hash & oldCap == 0,true 原位置,false 原位置 + oldCap

    oldCap 二进制该 bit 为 1,1 & 任何数都是该数本身,所以可以用来检查 hash 该 bit 是 0 还是 1

    img

有两种情况会调用 resize:

  1. 懒加载,首次调用 put 时,用 resize 进行初始化,默认大小为 16,也可以在构造时指定值,实际大小为大于该值的 2 的 n 次方;
  2. size 超过 threshold 时,用 resize 进行扩容。

扩容是一个特别耗性能的操作,所以实际使用 HashMap 时,最好估算 map 大小,初始化给一个大致数值,避免频繁的扩容。

HashMap之resize详解

长度为 2 的整数次方

Q:为什么 HashMap 的数组长度是 2 的整数次方?

主要为了优化寻址,当 length 为 2 的 n 次方时, (length - 1) & hash 等同于对 length 取模,但性能更好。

1.8 的优化

Q:1.8 做了哪些优化?为什么这么做?

  1. 增加了红黑树:当链表长度大于 8 时转换为红黑树,来提高 put、get 效率到 O(logN);

  2. 优化高位运算:改成一次,让其高 16 位异或低 16 位,使得后续寻址时,即使 length 比较小,也能保证高低位都参与哈希计算,减少哈希冲突;

  3. 链表插入方式改为尾插法;

  4. 扩容时,采用更简单的判断逻辑,在原位置或原位置 + 旧容量,这结合尾插法,解决了多线程死循环问题。

Hashtable

Q:HashMap 与 Hashtable 的区别?

Hashtable:基于哈希表实现,不支持 null 键和值,由于同步导致性能较差,不推荐使用;推荐使用基于分段锁实现的 ConcurrentHashMap 来支持线程安全,性能更好。

HashMap:与 Hashtable 类似,基于哈希表实现,但支持 null 键和值,并且不是线程安全的,性能更好;散列正常时,能提供常数时间的 put / get 操作,但不保证有序;如果需要满足线程安全,可以用 Collections.synchronizedMap(),或者直接用性能更好的 ConcurrentHashMap。

线程安全性

Q:HashMap 在并发下有什么问题?(HashMap 是线程安全的吗?)

HashMap 不是线程安全的,所以在并发下:

  • 1.7 会因头插法和 rehash 形成一个循环链表,造成死循环问题,以及多线程 put 会造成数据丢失和覆盖的问题;

  • 1.8 做了优化,改用尾插法,以及扩容后元素在原位置或原位置+旧容量,解决了死循环问题,但仍有数据丢失和覆盖的问题。

Q:你平常怎么解决这个线程不安全的问题?

想线程安全的使用 HashMap,有三种方式:

  1. Hashtable:直接在方法上加 synchronized,锁住整个数组,性能很低;
  2. Collections.synchronizedMap():使用对象锁,性能也很低;
  3. 推荐使用 ConcurrentHashMap:使用分段锁,降低了锁粒度,大大提高了性能。

节点无序

Q:HashMap 内部节点是有序的吗?

是无序的,根据 hash 值随机插入

Q:那有没有有序的 Map ?

LinkedHashMap 和 TreeMap

  • LinkedHashMap:HashMap 的子类,底层使用双向链表维护元素插入的顺序,也可以构造时设置 accessOrder 为 true,转换成访问顺序;同时能提供常数时间的 put / get 操作,但性能略低于 HashMap,因为有需要维护链表的开销。

  • TreeMap:基于红黑树实现,支持顺序访问,默认按键值升序,也可以由指定的 Comparator 来决定,但 put / get 效率为 O(logN);使用时,key 必须实现 Comparable 接口或者在构造时传入指定的 Comparator,否则运行时会抛出 ClassCastException。

Java 8系列之重新认识HashMap

一个HashMap跟面试官扯了半个小时

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值