HashMap
在新增元素时,会调用put
方法,本质上调用了putVal
方法:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
在调用putval
方法时传入了5
个参数:
- 第一个参数
hash
值:调用了hash
方法计算了Key
的hash
值 - 第二个参数
key
:就是我们传入的key
值 - 第三个参数
value
:就是我们传入的value
值 - 第四个参数
onlyIfAbsent
:也就是当键相同时,不修改已存在的值 - 第五个参数
evict
:如果为false
,那么数组就处于创建模式中,所以一般为true
。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
JDK默认提供的hashCode方法是根据对象的内存地址计算得到的。在Object类中,hashCode方法的实现如下:
public native int hashCode();
JDK默认提供的hashCode方法返回值是怎么计算得到的?
public native int hashCode();
这里的native
关键字表示该方法是由本地代码实现的,具体的实现细节是由底层的虚拟机提供的。
当我们调用一个对象的hashCode
方法时,虚拟机会根据对象的内存地址计算出一个整数值作为该对象的哈希码(HashCode
)。每个对象在内存中都有一个唯一的地址,因此不同的对象一般会有不同的哈希码。
需要注意的是,由于hashCode
是根据内存地址计算的,因此在不同的虚拟机实现中,同一个对象的哈希码可能会有所差异。另外,如果我们在自定义的类中没有重写hashCode
方法,那么默认会使用Object
类中的hashCode
方法,即根据内存地址计算。
在实际开发中,如果我们希望根据对象的内容计算哈希码而不是内存地址,就需要重写hashCode
方法,并根据对象的属性值计算出一个合适的哈希码。这样可以确保相等的对象具有相等的哈希码,从而在使用哈希表等数据结构时能够正常工作。
注意在HashMap
的put
方法在第一次向table
中放入元素时,才会对table
进行初始化操作。进入putVal
方法中:
// hash:关于Key的hash码 // key,value // onlyIfAbsent:标识,当存在相同的Key时,是否修改Value值 // evict:标识,数组是否处于创建模式 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {、 // 声明了一个局部变量:tab(Node数组类型) Node<K,V>[] tab; // Node指针p: Node<K,V> p; int n, i; // 把HashMap中维护的table数组赋值给了tab局部变量,然后判断是否为空 // 如果tab为空,意味着table也为空 // n 赋值调用resize()方法【见下resize方法解析】 if ((tab = table) == null || (n = tab.length) == 0) // 完成resize() // 第一次put时返回的是newTab,这里获得的长度默认是16,n = 16 n = (tab = resize()).length; // i = (n - 1) & hash:将给定的哈希值与数组的长度进行按位与操作, // 并将结果作为索引位置。这个操作的目的是根据哈希值的特征, // 将元素尽可能均匀地分布在不同的桶中,以提高HashMap的性能和效率。 if ((p = tab[i = (n - 1) & hash]) == null) // 如果在索引位置i上没有节点,则说明没有出现Hash冲突,就newNode新建一个节点 tab[i] = newNode(hash, key, value, null); // 如果出现Hash冲突的情况,就需要进行链表尾插,如果达到扩容阈值,还会进行后续的扩容操作 // (第一次put元素就不细看这部分代码,在扩容时细看) else { // Hash冲突,链表追加元素,扩容操作...... } // modCount 记录HashMap的结构修改次数的计数器 // 作用是在迭代器遍历HashMap的过程中,用于检测并发修改异常 // (ConcurrentModificationException)。 ++modCount; // 如果table的长度达到阈值,如果达到则需要进行resize扩容操作 if (++size > threshold) resize(); // afterNodeInsertion 方法 // 在插入新节点后负责处理可能发生的扩容和链表转换为红黑树的操作, // 以及更新相应的哈希表字段。这些操作保证了HashMap的性能和正确性。 afterNodeInsertion(evict); return null; }
resize()
方法:
在第一次进行put
操作时,主要是初始化table
,返回新创建的newTab
数组。这个方法的主要作用是对table
数组进行扩容操作。
final Node<K,V>[] resize() { // oldTab 指针,指向table数组: Node<K,V>[] oldTab = table; // 获取table数组长度 // 如果时第一次put值进入这个方法的话,此时table为null,oldTab == null 为true,oldCap值返回0 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 记录HashMap中维护的threshold值,这个追维护了哈希表的容量阈值 // 当HashMap中的元素数量达到threshold值时,会触发哈希表的扩容操作 int oldThr = threshold; int newCap, newThr = 0; // oldCap,原table的容量,大于0代表table中有元素存在 // 第一次put时,oldCap为0 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 第一次put时,会走到这一个分支操作: else { // zero initial threshold signifies using defaults // DEFAULT_INITIAL_CAPACITY 默认值是 16 newCap = DEFAULT_INITIAL_CAPACITY; // 新的扩容阈值被更新 // 默认情况下:DEFAULT_LOAD_FACTOR 是 0.75 // 计算出的 newThr 就是 12 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 如计算出来新的threshold值是0的话,新的容量 * 负载因子作为扩容阈值: if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 更新阈值: threshold = newThr; // 创建新的table数组: @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 进行新旧table的的赋值操作: table = newTab; // 只有旧table有值时才会进行接下来的操作 // 如果是第一次put操作,此时 oldTab == null if (oldTab != null) { // 这里是进行扩容操作的代码片段,后面在分析扩容时会详细阅读!!! } // 返回新的table数组: return newTab; }
afterNodeInsertion
方法:
在HashMap
中定义的一个空方法,具体实现是由LinkedHashMap
实现的:
void afterNodeInsertion(boolean evict) { }
以下是Linked HashMap
实现的afterNodeInsertion
方法:
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
HashMap
中的afterNodeInsertion
方法是一个钩子方法(hook method
),用于在插入新节点后执行特定的操作。具体来说,它在putVal方法中被调用,用于处理插入新节点后的后续操作。
afterNodeInsertion
方法的作用包括:
- 扩容:在插入节点后,如果哈希表的负载因子(
load factor
)超过了阈值,就会触发扩容操作。afterNodeInsertion
方法会检查当前节点数量是否达到了扩容的条件,并在需要时调用resize方法进行扩容。 - 链表转换为红黑树:如果插入节点后,某个链表的长度超过了阈值(默认为
8
),且当前哈希表的容量超过了64
,就会触发将链表转换为红黑树的操作。afterNodeInsertion
方法会检查当前链表的长度,并在需要时调用treeifyBin
方法将链表转换为红黑树。 - 重新计算哈希表的字段:插入新节点后,哈希表的节点数量会增加,因此需要更新哈希表的字段,如size(节点数量)和
modCount
(修改次数)等。
总的来说,afterNodeInsertion
方法在插入新节点后负责处理可能发生的扩容和链表转换为红黑树的操作,以及更新相应的哈希表字段。这些操作保证了HashMap
的性能和正确性。