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) 比较

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) {
  • 不会缩容
  • 树太小时,会解除树化

参考资料

posted @ 2023-06-20 11:23  流星<。)#)))≦  阅读(63)  评论(0编辑  收藏  举报