哈喽,各位 Java 学习者!欢迎来到《Java 学习日记》的第十五篇内容~ 前面我们掌握了 List(有序可重复)和 Set(无序不可重复),今天要攻克集合框架的核心 ------Map接口,以及它的最常用实现类HashMap。Map 是 "键值对(Key-Value)" 存储的容器,是开发中处理 "映射关系"(如用户 ID→用户信息、商品 ID→商品价格)的核心工具,而 HashMap 的底层原理更是 Java 面试的高频考点。本文会从 Map 的核心特性、HashMap 的底层实现(数组 + 链表 + 红黑树)、哈希冲突解决,到实战场景和性能优化,帮你彻底掌握 HashMap!
一、Map 接口:键值对存储的核心
1. 为什么需要 Map?
List 和 Set 只能存储单一元素,但开发中大量场景需要 "一对一映射":
- 用户 ID(String)→ 用户对象(User);
- 商品 ID(Integer)→ 商品价格(Double);
- 配置项名称(String)→ 配置值(String)。
Map 的核心价值就是通过键(Key)快速查找值(Value),且保证 Key 的唯一性(类似 Set),Value 可重复。
2. Map 接口的核心特性
| 特性 | 说明 |
|---|---|
| 键值对存储 | 每个元素由 Key 和 Value 组成,Key 是唯一标识,Value 是对应数据 |
| Key 唯一性 | 不允许重复 Key(重复添加会覆盖原有 Value) |
| Value 可重复性 | 允许多个 Key 映射到同一个 Value |
| 无序性 | (HashMap)不保证插入顺序,由哈希值决定;(TreeMap)按 Key 排序 |
| 允许 null | (HashMap)允许一个 null Key、多个 null Value |
| 无索引 | 不支持索引访问,只能通过 Key 获取 Value |
3. Map 接口的常用方法(必背)
Map 是独立的顶层接口(不继承 Collection),核心方法如下:
| 方法 | 作用 | 示例 |
|---|---|---|
put(K key, V value) |
添加 / 修改键值对(Key 存在则覆盖 Value) | map.put("name", "张三") |
get(Object key) |
根据 Key 获取 Value(不存在则返回 null) | map.get("name") |
remove(Object key) |
根据 Key 删除键值对(返回被删除的 Value) | map.remove("name") |
containsKey(Object key) |
判断是否包含指定 Key | map.containsKey("name") |
containsValue(Object value) |
判断是否包含指定 Value | map.containsValue("张三") |
size() |
获取键值对个数 | map.size() |
isEmpty() |
判断是否为空 | map.isEmpty() |
clear() |
清空所有键值对 | map.clear() |
keySet() |
获取所有 Key 的 Set 集合 | Set<String> keys = map.keySet() |
values() |
获取所有 Value 的 Collection 集合 | Collection<String> values = map.values() |
entrySet() |
获取所有键值对(Entry)的 Set 集合 | Set<Entry<String,String>> entrySet = map.entrySet() |
实战 1:Map 接口基础用法
java
运行
java
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapBasicDemo {
public static void main(String[] args) {
// 1. 创建Map对象(多态:父接口引用指向子类实现)
Map<String, String> userMap = new HashMap<>();
// 2. 添加键值对(Key唯一,Value可重复)
userMap.put("name", "张三");
userMap.put("age", "20");
userMap.put("gender", "男");
userMap.put("age", "21"); // Key重复,覆盖原有Value
userMap.put(null, "空键值"); // 允许null Key
userMap.put("hobby", null); // 允许null Value
System.out.println("Map大小:" + userMap.size()); // 5
// 3. 获取值
System.out.println("姓名:" + userMap.get("name")); // 张三
System.out.println("不存在的Key:" + userMap.get("address")); // null
// 4. 遍历Map(三种方式)
// 方式1:遍历Key,通过Key获取Value(效率低,需多次get)
System.out.println("\n遍历Key:");
Set<String> keys = userMap.keySet();
for (String key : keys) {
System.out.println(key + " → " + userMap.get(key));
}
// 方式2:遍历Value(无法获取Key)
System.out.println("\n遍历Value:");
Collection<String> values = userMap.values();
for (String value : values) {
System.out.println(value);
}
// 方式3:遍历Entry(推荐,一次获取Key+Value)
System.out.println("\n遍历Entry(推荐):");
Set<Map.Entry<String, String>> entrySet = userMap.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}
// 5. 其他常用方法
System.out.println("\n是否包含Key=name:" + userMap.containsKey("name")); // true
System.out.println("是否包含Value=21:" + userMap.containsValue("21")); // true
userMap.remove("gender");
System.out.println("删除gender后大小:" + userMap.size()); // 4
}
}
二、HashMap 底层原理(面试核心)
1. 核心结构(Java 8+)
HashMap 的底层是 "数组 + 链表 + 红黑树" 的组合结构,核心设计目标是平衡 "查询效率" 和 "空间利用率":
- 数组(哈希桶 /table):核心存储结构,每个数组元素是一个链表 / 红黑树的头节点;
- 链表:解决哈希冲突(不同 Key 的哈希值相同),当链表长度≤8 时使用;
- 红黑树:当链表长度 > 8 且数组长度≥64 时,链表转为红黑树,提升查询效率(O (n)→O (log n))。
2. 核心参数
| 参数 | 说明 | 默认值 |
|---|---|---|
initialCapacity |
初始容量 | 16(必须是 2 的幂) |
loadFactor |
加载因子 | 0.75(扩容阈值 = 容量 × 加载因子) |
threshold |
扩容阈值 | 16×0.75=12(元素个数超过 12 则扩容) |
TREEIFY_THRESHOLD |
链表转红黑树阈值 | 8 |
UNTREEIFY_THRESHOLD |
红黑树转链表阈值 | 6 |
MIN_TREEIFY_CAPACITY |
树化的最小数组容量 | 64(避免数组过小就树化) |
3. 核心流程:put 方法执行步骤
HashMap 的put(K key, V value)是核心,完整执行流程如下:
完整可执行的 Mermaid 流程图代码
bash
graph TD
A[调用put(key, value)] --> B[计算Key的哈希值<br/>hash = key.hashCode() ^ (key.hashCode() >>> 16)]
B --> C[计算数组索引<br/>index = hash & (length-1)]
C --> D{索引位置是否为空?}
D -->|是| E[创建新节点,放入数组索引位置]
D -->|否| F{节点类型?}
F -->|红黑树| G[按红黑树规则添加节点]
F -->|链表| H{Key是否重复?<br/>(hash相等且equals为true)}
H -->|是| I[覆盖原有Value]
H -->|否| J[添加节点到链表尾部]
J --> K{链表长度>8?}
K -->|是| L{数组长度≥64?}
L -->|是| M[链表转红黑树]
L -->|否| N[数组扩容]
K -->|否| O[结束]
I --> P{元素个数>扩容阈值?<br/>(size > capacity×loadFactor)}
M --> P
N --> P
P -->|是| Q[数组扩容为原容量2倍<br/>重新计算所有节点索引]
P -->|否| O
Q --> O
E --> P
%% 样式优化(可选,提升可读性)
classDef step fill:#f9f,stroke:#333,stroke-width:1px
classDef judge fill:#9ff,stroke:#333,stroke-width:1px
class A,B,C,E,G,I,J,M,N,Q,O step
class D,F,H,K,L,P judge
关键步骤解析:
- 哈希值计算 :
hash = key == null ? 0 : (key.hashCode() ^ (key.hashCode() >>> 16))(高位异或低位,减少哈希冲突) - 索引计算 :
index = hash & (table.length - 1)(等价于取模,效率更高,要求容量是 2 的幂) - 哈希冲突解决:链地址法(链表 / 红黑树)
- 扩容:容量翻倍(2 倍),重新计算所有节点的索引,迁移数据。
4. 核心源码简化(Java 8+)
java
运行
java
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
// 哈希桶数组
transient Node<K,V>[] table;
// 元素个数
transient int size;
// 扩容阈值
int threshold;
// 加载因子
final float loadFactor;
// 节点类(链表节点)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 链表后继节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
// 红黑树节点类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左子节点
TreeNode<K,V> right; // 右子节点
TreeNode<K,V> prev; // 前驱节点
boolean red; // 红黑树颜色标记
}
// 添加元素核心方法
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) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 数组未初始化/为空,先扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算索引,索引位置为空则创建新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 3. 索引位置有节点,判断Key是否重复
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // Key重复,记录旧节点
// 4. 节点是红黑树,按红黑树规则添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 5. 节点是链表,遍历链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表长度>8,转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 链表中找到重复Key,跳出循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 6. Key重复,覆盖Value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
// 7. 元素个数+1,判断是否需要扩容
++modCount;
if (++size > threshold)
resize(); // 扩容为原容量2倍
return null;
}
// 扩容方法
final Node<K,V>[] resize() {
// 1. 计算新容量(原容量×2)
// 2. 计算新扩容阈值(新容量×加载因子)
// 3. 迁移原数组节点到新数组
// (源码省略,核心是重新计算索引并迁移)
return newTab;
}
}
三、HashMap 的核心特性与性能分析
1. 核心特性
| 特性 | 说明 |
|---|---|
| 底层结构 | 数组 + 链表 + 红黑树(Java 8+) |
| Key 唯一性 | 基于hashCode()+equals()判断 Key 是否重复 |
| 顺序 | 无序(不保证插入顺序,也不保证排序) |
| null 支持 | 允许一个 null Key、多个 null Value |
| 线程安全 | 非线程安全(多线程操作可能导致死循环、数据丢失) |
| 扩容机制 | 容量翻倍(2 倍),扩容阈值 = 容量 × 加载因子(默认 0.75) |
2. 时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| put/get/remove | O(1) | 无哈希冲突时,直接通过索引访问 |
| put/get/remove | O(n) | 极端哈希冲突(所有节点在一个链表) |
| put/get/remove | O(log n) | 红黑树场景(链表长度 > 8) |
3. 自定义对象作为 Key 的核心规则(面试高频)
当用自定义对象作为 HashMap 的 Key 时,必须重写hashCode()和equals(),否则会导致:
- 相同内容的对象被判定为不同 Key;
- 无法通过 get () 获取对应 Value。
实战 2:自定义对象作为 Key(重写 hashCode+equals)
java
运行
java
import java.util.HashMap;
import java.util.Map;
// 用户ID类:作为HashMap的Key,必须重写hashCode和equals
class UserId {
private long id; // 唯一标识
public UserId(long id) {
this.id = id;
}
// 重写equals:按id判断是否为同一个Key
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserId userId = (UserId) o;
return id == userId.id;
}
// 重写hashCode:基于id生成哈希值
@Override
public int hashCode() {
return Long.hashCode(id);
}
@Override
public String toString() {
return "UserId{" + "id=" + id + '}';
}
}
// 用户信息类
class UserInfo {
private String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "UserInfo{name='" + name + "', age=" + age + '}';
}
}
public class HashMapCustomKeyDemo {
public static void main(String[] args) {
Map<UserId, UserInfo> userMap = new HashMap<>();
// 创建两个内容相同的UserId对象
UserId id1 = new UserId(1001);
UserId id2 = new UserId(1001);
// 添加键值对
userMap.put(id1, new UserInfo("张三", 20));
// 用id2获取Value(重写hashCode+equals后可获取)
UserInfo info = userMap.get(id2);
System.out.println("通过id2获取的用户信息:" + info); // UserInfo{name='张三', age=20}
// 若未重写hashCode+equals,此处会返回null
}
}
四、HashMap 的性能优化(开发必知)
1. 初始化指定容量(核心优化)
HashMap 默认初始容量 16,若预估要存储 N 个元素,建议初始化容量为:initialCapacity = (int) (N / loadFactor) + 1(默认 loadFactor=0.75,避免扩容)
示例:
java
运行
java
// 预估存储1000个元素,避免扩容
int expectedSize = 1000;
int initialCapacity = (int) (expectedSize / 0.75) + 1;
Map<String, String> map = new HashMap<>(initialCapacity);
2. 选择合适的加载因子
- 加载因子越大:空间利用率越高,哈希冲突概率越大;
- 加载因子越小:哈希冲突概率越小,空间浪费越多;
- 开发中默认 0.75 是平衡选择,无需修改。
3. 避免使用可变对象作为 Key
若 Key 是可变对象(如 ArrayList),修改对象内容会导致 hashCode 变化,无法通过 get () 获取 Value:
java
运行
java
// 错误示例:ArrayList作为Key(可变)
Map<ArrayList<String>, String> map = new HashMap<>();
ArrayList<String> key = new ArrayList<>();
key.add("test");
map.put(key, "value");
key.add("modify"); // 修改Key内容,hashCode变化
System.out.println(map.get(key)); // null(无法获取)
4. 多线程场景替代方案
HashMap 非线程安全,多线程场景建议使用:
ConcurrentHashMap(Java 8+,分段锁 / CAS,性能高);- 避免使用
Hashtable(方法加 synchronized,性能低)。
五、HashMap 的高频坑点
1. 坑点 1:Key 为 null 的处理
- HashMap 允许一个 null Key,哈希值固定为 0,索引为 0;
- 避免在自定义 Key 的 hashCode () 中返回 0(增加哈希冲突概率)。
2. 坑点 2:扩容导致的线程安全问题
多线程下 put () 可能导致:
- 链表成环(死循环);
- 数据丢失;
- 解决方案:用 ConcurrentHashMap。
3. 坑点 3:遍历中修改 Map(并发修改异常)
增强 for 循环遍历 Map 时,调用 put/remove 会抛出ConcurrentModificationException:
java
运行
java
// 错误示例
for (Map.Entry<String, String> entry : map.entrySet()) {
if (entry.getKey().equals("test")) {
map.remove("test"); // 抛出异常
}
}
// 正确示例(使用迭代器)
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
if (entry.getKey().equals("test")) {
it.remove(); // 安全删除
}
}
4. 坑点 4:equals 和 hashCode 不一致
- 必须保证:
equals()返回 true 的对象,hashCode()必须相等; - 反之不要求(hashCode 相等,equals 可返回 false)。
六、综合实战:HashMap 实现商品库存管理
java
运行
java
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
// 商品库存管理系统
public class GoodsStockManager {
// Key:商品ID,Value:库存数量
private Map<String, Integer> stockMap;
public GoodsStockManager() {
// 预估存储100种商品,初始化容量
int initialCapacity = (int) (100 / 0.75) + 1;
stockMap = new HashMap<>(initialCapacity);
// 初始化库存
stockMap.put("G001", 100); // 手机
stockMap.put("G002", 50); // 电脑
stockMap.put("G003", 200); // 平板
}
// 增加库存
public void addStock(String goodsId, int count) {
if (count <= 0) {
System.out.println("增加数量必须大于0!");
return;
}
// 存在则累加,不存在则初始化
stockMap.put(goodsId, stockMap.getOrDefault(goodsId, 0) + count);
System.out.println("商品" + goodsId + "库存增加" + count + ",当前库存:" + stockMap.get(goodsId));
}
// 减少库存
public boolean reduceStock(String goodsId, int count) {
if (count <= 0) {
System.out.println("减少数量必须大于0!");
return false;
}
Integer currentStock = stockMap.get(goodsId);
if (currentStock == null) {
System.out.println("商品" + goodsId + "不存在!");
return false;
}
if (currentStock < count) {
System.out.println("商品" + goodsId + "库存不足!当前库存:" + currentStock);
return false;
}
stockMap.put(goodsId, currentStock - count);
System.out.println("商品" + goodsId + "库存减少" + count + ",当前库存:" + stockMap.get(goodsId));
return true;
}
// 查询库存
public void queryStock(String goodsId) {
Integer stock = stockMap.get(goodsId);
if (stock == null) {
System.out.println("商品" + goodsId + "不存在!");
} else {
System.out.println("商品" + goodsId + "当前库存:" + stock);
}
}
// 展示所有库存
public void showAllStock() {
System.out.println("\n===== 所有商品库存 =====");
for (Map.Entry<String, Integer> entry : stockMap.entrySet()) {
System.out.println("商品ID:" + entry.getKey() + ",库存:" + entry.getValue());
}
}
public static void main(String[] args) {
GoodsStockManager manager = new GoodsStockManager();
Scanner scanner = new Scanner(System.in);
// 模拟操作
manager.queryStock("G001"); // 查询库存
manager.addStock("G001", 50); // 增加库存
manager.reduceStock("G002", 60); // 库存不足
manager.reduceStock("G002", 20); // 减少库存
manager.showAllStock(); // 展示所有库存
scanner.close();
}
}
总结
关键点回顾
- Map 核心特性:键值对存储,Key 唯一、Value 可重复,无索引,是映射关系的核心容器;
- HashMap 底层:Java 8 + 是 "数组 + 链表 + 红黑树",通过链地址法解决哈希冲突;
- 核心规则 :自定义对象作为 Key 必须重写
hashCode()和equals(),保证 Key 唯一性; - 性能优化:初始化指定容量(避免扩容)、避免可变对象作为 Key;
- 线程安全:多线程用 ConcurrentHashMap 替代 HashMap。
HashMap 是 Java 集合框架中使用频率最高的实现类,掌握其底层原理和优化技巧,能让你的代码既高效又健壮。下一篇我们会讲解 "集合工具类(Collections)与 Stream 流入门",帮你简化集合操作、提升代码简洁性!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀