20230410 java.util.HashMap
问题
第一部分,基础入门:
1.数组的优势/劣势
2.链表的优势/劣势
3.有没有一种方式整合两种数据结构的优势?散列表
4.散列表有什么特点?
5.什么是哈希?
第二部分,HashMap原理讲解:
1.HashMap的继承体系是什么样的?
2.Node数据结构分析?
3.底层存储结构介绍?
4.put数据原理分析?
5.什么是Hash碰撞?
6.什么是链化?
7.jdk8为什么引入红黑树?
8.HashMap扩容原理?
第三部分,手撕源码:
1.HashMap核心属性分析(threshold, loadFactory, size, modCount)
2.构造方法分析
3.HashMap put 方法分析 => putVal方法分析
4.HashMap resize 扩容方法分析(核心)
5.HashMap get 方法分析
6.HashMap remove 方法分析
7.HashMap replace 方法分析
简介
java.util.HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
- Map 接口的基于哈希表的实现
- 不保证顺序会随着时间的推移保持不变
- HashMap 的实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中桶的数量,初始容量只是创建哈希表时的容量。负载因子是哈希表在其容量自动增加之前允许达到多满的度量。当哈希表的条目数超过负载因子与当前容量的乘积时,哈希表将被重新哈希(即重建内部数据结构),使哈希表的桶数大约增加一倍。
- 默认加载因子 (0.75)
- 线程不安全,快速失败
- 最大容量 \(2^{30}\)
核心属性
// 底层数组
transient Node<K,V>[] table;
// kv对个数
transient int size;
// HashMap 结构修改的次数,快速失败
transient int modCount;
// 扩容阈值(容量*负载因子)
// 始终是2的n次幂,参考 tableSizeFor 方法
int threshold;
// 哈希表的加载因子
final float loadFactor;
构造方法
- HashMap()
- HashMap(int initialCapacity)
- HashMap(int initialCapacity, float loadFactor)
- HashMap(Map<? extends K, ? extends V> m)
initialCapacity
初始容量
hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
该函数首先检查key是否为null,如果是,则返回0。否则,它使用key.hashCode()方法计算key的哈希码,并将其存储在变量h中。然后,它使用位运算符将h的高16位与低16位进行异或运算,以产生最终的哈希值。
这种哈希函数的设计是为了尽可能地减少哈希冲突的数量。通过将h的高16位与低16位进行异或运算,可以将h的所有位都参与到哈希值的计算中,从而减少哈希冲突的可能性。
put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
- 通过 hash & (cap-1) 确定key在数组中的位置,因为cap始终是2的n次幂,所以cap-1的二进制表示是n个1,hash的值截取n位就是在数组中的位置
- 当resize后,cap乘以2翻倍,hash的值截取n+1位,当第n+1位是1时,位置变化索引+cap,否则,位置不变
- 如果一个桶位的链表节点个数等于8(TREEIFY_THRESHOLD),对桶位上的节点进行树化 treeifyBin
get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
- key在数组中的桶索引是 (tab.length - 1) & hash
- 判断key相等:通过 == 和 equals 方法
- 判断桶中的节点是不是树节点
resize
- 作用有2个,扩容和重新hash
- 默认初始容量 16 (DEFAULT_INITIAL_CAPACITY),加载因子 0.75 (DEFAULT_LOAD_FACTOR),扩容阈值 12
- 对底层数组进行扩容,容量乘以2
- 如果桶位上只有1个节点,重新计算桶位,e.hash & (newCap - 1)
- 如果是桶上是树节点,调用 split 方法,将树桶中的节点拆分为较低和较高的树桶,或者如果现在太小则取消树化,untreeify
- 如果桶位上不是树节点,且不止1个节点,计算高位(hiHead, hiTail)和低位链表(loHead, loTail),e.hash & oldCap,分别放入 j + oldCap, j
treeifyBin
- 桶位小于64(MIN_TREEIFY_CAPACITY),不进行树化,而是进行一次扩容
- 调用 TreeNode#treeify 将桶内的节点红黑树化
- 红黑树根据key的hash大小比较进行排序
- 如果hash相等
- key 实现了 Comparable 接口,使用接口方法 compareTo 比较
- 如果 compareTo 比较相等,为了比较出大小,使用类名称(x.getClass().getName())比较
- 类名称相同,使用 System.identityHashCode(a) 比较
- 如果 compareTo 比较相等,为了比较出大小,使用类名称(x.getClass().getName())比较
- key 实现了 Comparable 接口,使用接口方法 compareTo 比较
remove
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
- 不会缩容
- 树太小时,会解除树化