【Day15】集合框架(三):Map 接口(HashMap 底层原理 + 实战)

哈喽,各位 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

关键步骤解析:

  1. 哈希值计算hash = key == null ? 0 : (key.hashCode() ^ (key.hashCode() >>> 16))(高位异或低位,减少哈希冲突)
  2. 索引计算index = hash & (table.length - 1)(等价于取模,效率更高,要求容量是 2 的幂)
  3. 哈希冲突解决:链地址法(链表 / 红黑树)
  4. 扩容:容量翻倍(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();
    }
}

总结

关键点回顾

  1. Map 核心特性:键值对存储,Key 唯一、Value 可重复,无索引,是映射关系的核心容器;
  2. HashMap 底层:Java 8 + 是 "数组 + 链表 + 红黑树",通过链地址法解决哈希冲突;
  3. 核心规则 :自定义对象作为 Key 必须重写hashCode()equals(),保证 Key 唯一性;
  4. 性能优化:初始化指定容量(避免扩容)、避免可变对象作为 Key;
  5. 线程安全:多线程用 ConcurrentHashMap 替代 HashMap。

HashMap 是 Java 集合框架中使用频率最高的实现类,掌握其底层原理和优化技巧,能让你的代码既高效又健壮。下一篇我们会讲解 "集合工具类(Collections)与 Stream 流入门",帮你简化集合操作、提升代码简洁性!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀

相关推荐
没有bug.的程序员2 小时前
熔断、降级、限流:高可用架构的三道防线
java·网络·jvm·微服务·架构·熔断·服务注册
派大鑫wink2 小时前
【Day14】集合框架(二):Set 接口(HashSet、TreeSet)去重与排序
java·开发语言
weixin_515069662 小时前
BeanToMapUtil-对象转Map
java·工具类·java常用api
code_std2 小时前
保存文件到指定位置,读取/删除指定文件夹中文件
java·spring boot·后端
sort浅忆2 小时前
deeptest执行接口脚本,添加python脚本断言
开发语言·python
趣知岛2 小时前
JavaScript性能优化实战大纲
开发语言·javascript·性能优化
小许学java2 小时前
Spring事务和事务传播机制
java·数据库·spring·事务
大学生资源网2 小时前
基于Javaweb技术的宠物用品商城的设计与实现(源码+文档)
java·mysql·毕业设计·源码·springboot
汤姆yu2 小时前
基于springboot的热门文创内容推荐分享系统
java·spring boot·后端