HashMap 主要内容:
- 基本数据结构
- 树化与退化
- 索引计算
- put 与扩容
- 并发问题
- key 的设计
1. 基本数据结构🛠️
HashMap 的底层是基于数组加链表的数据结构📊。每个数组的槽位(bucket)可以存储多个键值对,这些键值对通过链表连接起来。当多个键的哈希值相同(即哈希冲突发生时),这些键值对会形成一个链表🔗。
生活例子🍔: 想象你在超市的货架上找商品,每个货架都是一个数组槽位。每个货架上可以放多个不同商品(键值对),如果商品过多,货架上的商品就用链条连接起来,一直挂下去(链表)🛒。
2. 树化与退化🌳
当一个桶(bucket)中的链表长度超过一定的阈值时(默认是8),链表会自动转变成一个红黑树🌳,这样可以提高查询效率。如果后期元素减少,树也可以退化为链表。这是为了在元素多时保持快速的查询性能,而避免链表遍历导致性能下降⏳。
生活例子🍰: 想象你在超市排队💡,如果队伍太长,店员可能会引导大家站成多个队伍,或者打开更多的收银台(树化)🏪。当顾客减少时,队伍又可以合并为一条(退化)💁♀️。
3. 索引计算🧮
HashMap 使用哈希函数将键(key)映射到数组中的某个位置(索引)。具体来说,HashMap 会通过 hashCode()
方法计算键的哈希值,并通过取模操作确定键值对应该放到哪个桶(bucket)里📥。
生活例子🍔: 想象你有一张超市的地图🛒,每个货架上都有一个编号(索引)。你根据商品的特性(哈希值)来决定它应该放在哪个货架上。这个货架编号就是通过索引计算出来的。
4. put 与扩容📦
每次我们调用 put()
方法往 HashMap 中放入一个键值对时,HashMap 首先计算键的哈希值,找到对应的槽位(bucket),然后将键值对存入。如果 HashMap 中的元素太多导致负载因子(load factor)超出预设值(通常是0.75),HashMap 会进行扩容,即增加数组的大小,并将所有现有的键值对重新分布🔧。
生活例子🍰: 假设你在收拾超市的货架📦,如果货架上的商品越来越多,超市可能需要增加新的货架,并把现有商品重新摆放在新货架上,以避免货架拥挤。
5. 并发问题🔗
HashMap 不是线程安全的❌,也就是说在多线程环境下同时对 HashMap 进行修改可能会导致数据不一致、死循环等问题。为了解决这个问题,Java 提供了 ConcurrentHashMap 这样的线程安全版本来处理并发场景🏗️。
生活例子🍔: 想象多个顾客同时在超市里搬商品📦,如果没有人协调,他们可能会把商品放乱,甚至会导致混乱的局面。这个时候就需要一名管理员(ConcurrentHashMap)来管理这个过程。
6. key 的设计🗝️
为了提高 HashMap 的性能,设计键(key)时要确保 hashCode()
和 equals()
方法被正确实现。如果键的哈希值分布不均匀,会导致某些槽位(bucket)上的链表过长,从而影响性能🔧。理想情况下,每个键的哈希值都应该均匀地分布在 HashMap 中。
生活例子🍰: 就像你在超市中放商品一样,如果你把很多东西都放在同一个货架上,那货架就会特别拥挤,找东西也很慢⏳。所以最好每个货架上的商品分布均匀一些,这样大家找商品就方便多了🛒。
总结大比拼🏆:
知识点 | 解释 |
---|---|
基本数据结构 | 数组 + 链表结构,当冲突发生时,链表存储多个键值对。 |
树化与退化 | 链表长度超过阈值时会转换为红黑树,提升查询效率,减少时退化为链表。 |
索引计算 | 哈希函数计算键的哈希值,通过索引决定键值对的存储位置。 |
put 与扩容 | 插入时会根据负载因子进行扩容,重新分布现有键值对。 |
并发问题 | HashMap 在多线程下不安全,需要使用线程安全的 ConcurrentHashMap。 |
key 的设计 | 设计好的键要确保 hashCode() 和 equals() 方法,实现均匀分布。 |
代码展示一下:
包括 put()
操作、扩容、key 的设计等方面
HashMap 基础代码示例 + 详细注释 📝
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// 🛠️ 创建一个 HashMap,key 是字符串,value 是整数
HashMap<String, Integer> map = new HashMap<>();
// 📝 添加一些键值对到 HashMap 中
// put() 方法将键值对插入到 HashMap 中
// "Apple" 是 key,10 是 value
map.put("Apple", 10);
map.put("Banana", 20);
map.put("Orange", 30);
// 🔍 从 HashMap 中获取值
// get() 方法通过 key 获取对应的 value
int appleValue = map.get("Apple");
System.out.println("Apple 的值是: " + appleValue); // 输出: Apple 的值是: 10
// 📝 更新一个键的值
// put() 方法不仅用于插入,还可以用于更新
map.put("Apple", 15); // 将 "Apple" 的值更新为 15
System.out.println("更新后 Apple 的值是: " + map.get("Apple")); // 输出: 更新后 Apple 的值是: 15
// 📝 检查 HashMap 中是否包含某个 key
// containsKey() 方法返回 true 或 false
if (map.containsKey("Banana")) {
System.out.println("Banana 存在于 HashMap 中");
}
// 📝 删除键值对
// remove() 方法通过 key 删除相应的键值对
map.remove("Orange");
System.out.println("删除 Orange 后的 HashMap: " + map);
// 📝 遍历整个 HashMap
// 使用增强型 for 循环遍历所有的键值对
System.out.println("遍历 HashMap 的键值对:");
for (String key : map.keySet()) {
// keySet() 方法返回 HashMap 中所有的 key
int value = map.get(key); // 根据 key 获取对应的 value
System.out.println("Key: " + key + ", Value: " + value);
}
// 💡 模拟扩容
// 当元素过多时,HashMap 会自动扩容,重新分配空间并重新哈希键值对
// 这个例子模拟插入大量数据,看 HashMap 是否会自动扩容
for (int i = 0; i < 100; i++) {
map.put("Key" + i, i); // 添加 100 个键值对
}
System.out.println("插入 100 个键值对后的 HashMap 大小: " + map.size()); // 输出 HashMap 的大小
}
}
代码逐步解释 & 注释说明💡:
-
创建 HashMap 🛠️:
HashMap<String, Integer> map = new HashMap<>();
- 创建一个 HashMap 对象,其中 key 为字符串类型
String
,value 为整数类型Integer
。这是最基础的 HashMap 定义。
- 创建一个 HashMap 对象,其中 key 为字符串类型
-
插入键值对📝:
map.put("Apple", 10); map.put("Banana", 20); map.put("Orange", 30);
- 使用
put()
方法向 HashMap 中添加键值对。 put()
方法的作用是根据 key 将 value 存储起来,如果 key 已经存在,则更新其对应的值。
- 使用
-
获取键对应的值🔍:
int appleValue = map.get("Apple");
- 使用
get()
方法通过 key 获取对应的 value。如果 key 不存在,则返回 null。
- 使用
-
更新值📝:
map.put("Apple", 15);
- 如果 key 已经存在,再次调用
put()
方法会覆盖掉原来的值。这里把"Apple"
的值从10
更新为15
。
- 如果 key 已经存在,再次调用
-
检查 key 是否存在🔍:
if (map.containsKey("Banana")) { System.out.println("Banana 存在于 HashMap 中"); }
containsKey()
方法用来检查某个 key 是否存在于 HashMap 中,返回true
或false
。
-
删除键值对📝:
map.remove("Orange");
- 使用
remove()
方法通过 key 删除对应的键值对。
- 使用
-
遍历 HashMap 🛤️:
for (String key : map.keySet()) { int value = map.get(key); System.out.println("Key: " + key + ", Value: " + value); }
- 通过
keySet()
方法获取 HashMap 中所有的 key,并使用增强型for
循环遍历每个键值对。
- 通过
-
模拟扩容📦:
for (int i = 0; i < 100; i++) { map.put("Key" + i, i); } System.out.println("插入 100 个键值对后的 HashMap 大小: " + map.size());
- 当 HashMap 的大小超过负载因子(通常为 0.75)时,HashMap 会自动进行扩容。这个例子通过插入大量数据来模拟 HashMap 扩容的过程。