HashMap的put方法是如何实现的
- ✔️典型解析
- ✔️ 拓展知识仓
- ✔️HashMap put方法的优缺点有哪些
- ✔️如何避免HashMap put方法的哈希冲突
- ✔️如何避免HashMap put方法的哈希重
- ✔️源码解读
- ✔️putVal 方法主要实现如下,为了更好的帮助大家阅读,提升效率,每一行都特意加了注释
✔️典型解析
先看一个简化的put
方法实现,帮助理解其基本逻辑:
public V put(K key, V value) { // 1. 计算键的哈希码 int hash = hash(key); // 2. 计算桶的位置 int index = indexFor(hash, table.length); // 3. 检查桶中是否有键值对 for (Entry<K,V> e = table[index]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { // 如果找到相同的键,则更新值 return e.setValue(value); } } // 4. 如果桶为空或者找到了空位,插入新的键值对 addEntry(hash, key, value, index); return null; // 表示新插入的键值对,其值是null
}
下面是JDK 1.8中HashMap的put方法的简要实现过程:
1 . 首先,put方法会计算键的哈希值(通过调用hash方法),并通过哈希值计算出在数组中的索引位置。
2 . 如果该位置上的元素为空,那么直接将键值对存储在该位置上.
3 . 如果该位置上的元素不为空,那么遍历该位置上的元素,如果找到了与当前键相等的键值对,那么将该键值对的值更新为当前值,并返回旧值。
4 . 如果该位置上的元素不为空,但没有与当前键相等的键值对,那么将键值对插入到链表或红黑树中(如果该位置上的元素数量超过了一个闻值,就会将链表转化为红黑树来提高效率)。
5 . 如果插入成功,返回被替换的值:如果插入失败,返回null.
6 . 插入成功后,如果需要扩容,那么就进行一次扩容操作。
✔️ 拓展知识仓
✔️HashMap put方法的优缺点有哪些
HashMap的put方法在实现上具有一些优点和缺点。以下是一些可能的优缺点:
优点:
- 高效性:HashMap使用哈希表作为其底层实现,使得插入、删除和查找操作的时间复杂度接近O(1)。这使得HashMap在处理大量数据时具有很高的效率。
- 易于使用:HashMap提供了简单的方法来存储和检索数据,如put和get方法。这些方法易于使用,并且可以快速地添加新数据或检索现有数据。
- 动态扩展:HashMap可以根据需要自动调整其大小,以适应不同数量的数据。这使得HashMap能够处理任意数量的数据,而无需手动管理内存。
缺点:
- 哈希冲突:如果两个键的哈希码相同,则会发生哈希冲突。虽然HashMap使用链表或红黑树来解决冲突,但哈希冲突可能导致性能下降,特别是在高负载情况下。
- 线程不安全:HashMap不是线程安全的。如果在多线程环境中使用HashMap,需要额外的同步机制来保证线程安全。这可能导致性能下降和代码复杂度增加。
- 内存消耗:HashMap使用内存来存储键值对和哈希表结构。如果存储大量数据,可能会占用大量内存。
以上是HashMap put方法的一些优缺点。在实际应用中,选择合适的数据结构非常重要,需要根据具体需求进行权衡和选择。
✔️如何避免HashMap put方法的哈希冲突
哈希冲突是HashMap实现中不可避免的问题,但是可以通过一些策略来减少其影响:
- 选择合适的哈希函数:哈希函数是将键映射到桶的函数,选择一个好的哈希函数可以减少哈希冲突的发生。尽可能选择能够均匀分布键的哈希函数,以减少冲突的可能性。
- 使用链表或红黑树:当发生哈希冲突时,HashMap可以使用链表或红黑树来存储具有相同哈希码的键值对。链表可以存储多个键值对,而红黑树则可以平衡树中的节点,提高查找效率。
- 调整数组大小:如果哈希冲突过多,可以考虑增加HashMap的数组大小。这将重新分配内存,并可能导致性能下降。因此,应该谨慎地调整数组大小,以平衡性能和内存使用。
- 使用开放地址法:开放地址法是一种解决哈希冲突的方法,其中当发生冲突时,会在哈希表中寻找下一个可用的位置来存储键值对。这种方法可以减少哈希冲突的发生,但也可能导致性能下降。
- 合理设置负载因子:负载因子是已存储的键值对数量与桶的数量之比。如果负载因子过高,可能会导致哈希冲突增多。合理设置负载因子可以平衡性能和内存使用。
避免HashMap put方法的哈希冲突需要选择合适的哈希函数、使用链表或红黑树、调整数组大小、使用开放地址法和合理设置负载因子等方法。在实际应用中,需要根据具体情况进行权衡和选择。
✔️如何避免HashMap put方法的哈希重
避免HashMap的哈希冲突的关键在于设计一个好的哈希函数,使得键的哈希码尽可能均匀分布,减少冲突的可能性。以下是一些避免哈希冲突的策略:
-
选择合适的哈希函数:尽可能选择能够均匀分布键的哈希函数。哈希函数的设计应该考虑键的特性,使得不同的键能够产生不同的哈希码。
-
使用链表或红黑树:当发生哈希冲突时,HashMap可以使用链表或红黑树来存储具有相同哈希码的键值对。链表可以存储多个键值对,而红黑树则可以平衡树中的节点,提高查找效率。
-
调整数组大小:如果哈希冲突过多,可以考虑增加HashMap的数组大小。这将重新分配内存,并可能导致性能下降。因此,应该谨慎地调整数组大小,以平衡性能和内存使用。
- 合理设置负载因子:负载因子是已存储的键值对数量与桶的数量之比。如果负载因子过高,可能会导致哈希冲突增多。合理设置负载因子可以平衡性能和内存使用。
- 使用再哈希策略:当发生哈希冲突时,可以考虑使用再哈希策略。即当发生冲突时,使用另一个哈希函数计算新的哈希码,并尝试在新的位置存储键值对。这种方法可以减少冲突的可能性,但也可能导致性能下降。
综上所述,避免HashMap的哈希冲突需要选择合适的哈希函数、使用链表或红黑树、调整数组大小、合理设置负载因子和使用再哈希策略等方法。在实际应用中,需要根据具体情况进行权衡和选择。
看一个Demo:
/**
* 使用HashMap存储用户信息,并避免哈希冲突
*/import java.util.HashMap;public class UserInfo {private String username;private String email;private int age;public UserInfo(String username, String email, int age) {this.username = username;this.email = email;this.age = age;}// getter和setter方法省略@Overridepublic int hashCode() {int result = 17;result = 31 * result + username.hashCode();result = 31 * result + email.hashCode();result = 31 * result + age;return result;}@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;UserInfo other = (UserInfo) obj;return age == other.age && username.equals(other.username) && email.equals(other.email);}
}
Demo中,定义了一个UserInfo类,用于存储用户信息。该类具有username、email和age属性,并实现了hashCode和equals方法。hashCode方法根据username、email和age计算哈希码,以确保不同的UserInfo对象具有不同的哈希码。equals方法用于比较两个UserInfo对象是否相等。
接下来,创建一个HashMap对象,用于存储UserInfo对象:
import java.util.HashMap;
import java.util.Map;public class UserDatabase {private Map<String, UserInfo> database;public UserDatabase() {database = new HashMap<>();}public void addUser(UserInfo user) {database.put(user.getUsername(), user);}public UserInfo getUser(String username) {return database.get(username);}
}
在Demo中,我们创建了一个UserDatabase类,用于管理用户信息。该类使用Map<String, UserInfo>作为底层实现,其中键是用户名,值是UserInfo对象。addUser方法用于添加用户信息,getUser方法用于根据用户名检索用户信息。注意,我们使用了UserInfo对象的username属性作为键,以确保不同的用户具有不同的键。这样,即使两个UserInfo对象的email和age属性相同,但由于键不同,它们仍然会被视为不同的键值对。这样可以减少哈希冲突的可能性。
✔️源码解读
put方法的代码很简单,就一行代码:
public V put(K key,V value) {return putVal(hash(key), key,value, false, true);
}
核心其实是通过 putValue
方法实现的,在传给 putValue
的参数中,先调用 hash 获取了一下hashCode
。
【Java集合篇】HashMap的hash方法是如何实现的?
✔️putVal 方法主要实现如下,为了更好的帮助大家阅读,提升效率,每一行都特意加了注释
/**
* Implements Map.put and related methods.
*
* @param hash key 的 hash 值
* @param key key 值
* @param value value 值
* @param onlyIfAbsent true: 如果某个 key 已经存在那么就不插了; false 存在则替换,没有则新增。这里为 false
* @param evict 不用管了,我特喵也不认识
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// tab 表示当前 hash 散列表的引用Node<K, V>[] tab;//表示具体的散列表中的元素Node<k,V> p;//n:表示散列表数组的长度//i:表示路由寻址的结果int n, i;// 将 table 赋值发给 tab ;如果 tab == nul,说明 table 还没有被初始化。则此时是需要去创建 table 的//为什么这个时候才创建散列表因为可能创建了 HashMap 时候可能并没有存放数据,如果在初始化 ahMap 的时候就创建散列表,势必会造成空间的浪费//这里也就是延迟初始化的逻辑if ((tab = table) == null (n = tab.length) == 0) {n = (tab = resize()).length;}//如p == null说明寻址到的桶的位置没有元素。那么就将 key-value 封装到 Node 中,并放到寻址到的下标为的位置if ((p = tabli = (n - 1) & hash]) == null) {tab[i] = newNode(hash, key, value, null);}//到这里说明 该位置已经有数据了,且此时可能是链表结构,也可能是树结构 else {// e 表示找到了一个与当前要插入的key value 一致的元素Node<k,V> e;// 临时的 keyK k;//p 的值就是上一步 f 中的结果即: 此时的 (p = tab[i = (n - 1) & hash]) 不等于 null// p 是原来的已经在 位置的元素,且新插入的 key 是等于 p中的key// 说明找到了和当前需要插入的元素相同的元素(其实就是需要替换而已)if (p.hash == hash && ((k = p.key) == key (key != null && key.equals(k))))//将 p的值赋值给 ee =p;//说明已经树化,红黑树会有单独的文章介绍,本文不再整述else if ((p instanceof TreeNode) {e = ((Treelode<K, V>) p).putTreeVal(this, tab, hash, key, value);} else {//到这里说明不是树结构,也不相等,那说明不是同一个元素,那就是链表了for (int binCount = 0; : ++binCount) {//如果 p.next == nul1 说明 p 是最后一个元素,说明,该元素在链表中也没有重复的,那么就需要添加到链表的if ((e = p.next) == null) {//直接将 key-value 封装到 Node 中并且添加到 p的后面p.next = newNode(hash , key, value, null);//当元素已经是 7了,再来一个就是 8 个了,那么就需要进行树化if (binCount >= TREEIFY_THRESHOLD - 1) {treeifyBin(tab, hash);}break;}//在链表中找到了某个和当前元素一样的元素,即需要做替换操作了。if (e.hash == hash && ((k = e.key) == key (key != null && key.equals(k)))) {break:}//将e(即p.next)赋值为,这就是为了继续遍历链表的下一个元素(没啥好说的)下面有张图帮助大家理解p= e;}}//如果条件成立,说明找到了需要替换的数据,if (e != null) {//这里不就是使用新的值赋值为旧的值嘛V oldValue = e.value;if (!onlyIfAbsent || oldValue == null) {e.value = value;}//这个方法没用,里面啥也没有afterNodeAccess(e)://HashMap put 方法的返回值是原来位置的元素值return oldValue;}}/*** 上面说过,对于散列表的 结构修改次数,那么就修改 modCount 的次数*/++ modCount;//size 即散列表中的元素的个数,添加后需要自增,如果自增后的值大于扩容的阑值,那么就触发扩容操作if (++size > threshold) {resize();}//啥也没干afterNodeInsertion(evict);//原来位置没有值,那么就返回 nul1 呗return null;
}