1. 概述

本文将深入探讨Java集合框架中最流行的Map接口实现——HashMap的内部机制。作为Java开发者,掌握HashMap的底层原理至关重要,因为HashMap相关问题是面试高频考点

首先需要明确:List和Set集合接口都继承自Collection,但Map接口不继承Collection。HashMap通过键值对存储数据,提供添加、检索和操作数据的API。其实现基于哈希表原理,虽然听起来复杂,但实际非常直观:

✅ 键值对存储在"桶"(bucket)中
✅ 所有桶组成一个"表"(table)——本质是内部数组
✅ 在设计良好的HashMap中,存取操作时间复杂度为O(1)

要理解HashMap的工作原理,核心在于掌握其存储和检索机制。接下来我们将重点剖析这些机制。

2. put() API详解

向HashMap存储值时调用put方法,接收键和值两个参数:

V put(K key, V value);

执行流程:

  1. 调用key对象的hashCode()获取初始哈希值
  2. 通过内部hash()方法计算最终哈希值
  3. 根据最终哈希值确定桶位置(数组索引)
  4. 在桶中存储键值对(以Map.Entry对象形式)

关键点:HashMap将键和值都存储在桶中,而非仅存储值。这也是Map接口不继承Collection的原因——它存储的是键值对而非单个元素。

2.1 null键处理

HashMap允许null键和null值:

@Test
public void givenNullKeyAndVal_whenAccepts_thenCorrect(){
    Map<String, String> map = new HashMap<>();
    map.put(null, null);
}

⚠️ 当键为null时:

  • 自动分配最终哈希值0
  • 成为底层数组的第一个元素
  • 不会调用hashCode()方法,避免NPE

2.2 返回值规则

put方法的返回值遵循以下规则:

  • 新增键值对:返回null
  • 覆盖已有键:返回旧值
  • 旧值为null:返回null(需用containsKey区分)
@Test
public void givenExistingKey_whenPutReturnsPrevValue_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("key1", "val1");
    String rtnVal = map.put("key1", "val2");
    assertEquals("val1", rtnVal); // 返回旧值
}

@Test
public void givenNewKey_whenPutReturnsNull_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    String rtnVal = map.put("key1", "val1");
    assertNull(rtnVal); // 新增返回null
}

3. get() API详解

检索值时调用get方法,传入键对象:

@Test
public void whenGetWorks_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("key", "val");
    String val = map.get("key");
    assertEquals("val", val);
}

执行流程:

  1. 调用key对象的hashCode()获取初始哈希值
  2. 通过内部hash()方法计算最终哈希值
  3. 定位到目标桶
  4. 在桶中查找并返回值

3.1 null返回值处理

get返回null有两种情况:

  1. 键不存在
  2. 键存在但值为null

使用containsKey区分:

@Test
public void whenContainsDistinguishesNullValues_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    
    // 情况1:键不存在
    String val1 = map.get("key");
    boolean valPresent = map.containsKey("key");
    assertNull(val1);
    assertFalse(valPresent);

    // 情况2:键存在但值为null
    map.put("key", null);
    String val = map.get("key");
    valPresent = map.containsKey("key");
    assertNull(val);
    assertTrue(valPresent);
}

4. HashMap中的集合视图

HashMap提供三种集合视图,允许将键、值或条目视为集合:

4.1 键集合视图

@Test
public void givenHashMap_whenRetrievesKeyset_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("name", "baeldung");
    map.put("type", "blog");

    Set<String> keys = map.keySet();
    assertEquals(2, keys.size());
    assertTrue(keys.contains("name"));
    assertTrue(keys.contains("type"));
}

⚠️ 重要特性:键集合是映射的后备视图,修改集合会直接影响映射:

@Test
public void givenKeySet_whenChangeReflectsInMap_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("name", "baeldung");
    map.put("type", "blog");
    
    Set<String> keys = map.keySet();
    keys.remove("name"); // 通过集合删除键
    
    assertEquals(1, map.size()); // 映射同步更新
}

4.2 值集合视图

@Test
public void givenHashMap_whenRetrievesValues_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("name", "baeldung");
    map.put("type", "blog");

    Collection<String> values = map.values();
    assertEquals(2, values.size());
    assertTrue(values.contains("baeldung"));
    assertTrue(values.contains("blog"));
}

4.3 条目集合视图

@Test
public void givenHashMap_whenRetrievesEntries_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("name", "baeldung");
    map.put("type", "blog");

    Set<Entry<String, String>> entries = map.entrySet();
    assertEquals(2, entries.size());
    
    for (Entry<String, String> e : entries) {
        String key = e.getKey();
        String val = e.getValue();
        assertTrue(key.equals("name") || key.equals("type"));
        assertTrue(val.equals("baeldung") || val.equals("blog"));
    }
}

4.4 迭代器特性

所有集合视图的迭代器都是**快速失败(fail-fast)**的:

@Test(expected = ConcurrentModificationException.class)
public void givenIterator_whenFailsFastOnModification_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("name", "baeldung");
    map.put("type", "blog");

    Set<String> keys = map.keySet();
    Iterator<String> it = keys.iterator();
    map.remove("type"); // 迭代器创建后修改映射
    
    while (it.hasNext()) {
        String key = it.next(); // 抛出ConcurrentModificationException
    }
}

✅ 允许的修改:仅通过迭代器自身的remove()操作:

@Test
public void givenIterator_whenRemoveWorks_thenCorrect() {
    Map<String, String> map = new HashMap<>();
    map.put("name", "baeldung");
    map.put("type", "blog");

    Set<String> keys = map.keySet();
    Iterator<String> it = keys.iterator();
    
    while (it.hasNext()) {
        it.next();
        it.remove(); // 安全删除
    }
    
    assertEquals(0, map.size());
}

⚠️ 性能提示:HashMap迭代性能较差,最坏情况时间复杂度O(n),n为容量+条目数之和。

5. HashMap性能分析

HashMap性能受两个参数影响:

  • **初始容量(Initial Capacity)**:桶数量(底层数组长度)
  • **负载因子(Load Factor)**:扩容阈值(默认0.75)

5.1 参数设置

// 自定义初始容量
Map<String,String> hashMapWithCapacity = new HashMap<>(32);

// 自定义初始容量和负载因子
Map<String,String> hashMapWithCapacityAndLF = new HashMap<>(32, 0.5f);

5.2 扩容机制

当条目数 > 容量 × 负载因子时触发**扩容(rehashing)**:

  1. 创建新数组(容量翻倍)
  2. 将所有条目重新分配到新桶位置

⚠️ 扩容代价高昂,应合理设置初始容量。

5.3 容量选择策略

场景 初始容量 负载因子 原因
大量数据 + 少量迭代 默认 减少扩容次数
少量数据 + 频繁迭代 默认 节省内存,迭代更快

容量计算公式(Java 19前):

int capacity = (int)(expectedMappings / 0.75) + 1;

Java 19+优化:使用HashMap.newHashMap(int mappings)直接指定预期条目数:

Map<String, String> map = HashMap.newHashMap(10); // 直接指定条目数

6. HashMap中的哈希冲突

哈希冲突指多个键产生相同最终哈希值,导致映射到同一桶位置。

6.1 冲突原因

  • 不同对象可能有相同哈希码(equals/hashCode契约允许)
  • 底层数组大小有限

6.2 冲突解决机制

Java 8前:使用链表解决冲突
Java 8+:当冲突数超过阈值时,链表转为平衡二叉树

冲突处理流程

  1. 通过哈希值定位桶
  2. 遍历桶内元素
  3. 使用equals()比较键
  4. 找到匹配键则更新值,否则新增条目

6.3 冲突示例

自定义键类强制产生冲突:

public class MyKey {
    private String name;
    private int id;

    public MyKey(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    @Override
    public int hashCode() {
        System.out.println("Calling hashCode()");
        return id; // 强制相同id产生相同哈希码
    } 

    @Override
    public boolean equals(Object obj) {
        System.out.println("Calling equals() for key: " + obj);
        // 标准equals实现
    }
}

测试冲突处理:

@Test
public void whenCallsEqualsOnCollision_thenCorrect() {
    HashMap<MyKey, String> map = new HashMap<>();
    MyKey k1 = new MyKey(1, "firstKey");
    MyKey k2 = new MyKey(2, "secondKey");
    MyKey k3 = new MyKey(2, "thirdKey"); // 与k2同id

    map.put(k1, "firstValue");
    map.put(k2, "secondValue");
    map.put(k3, "thirdValue"); // 触发冲突

    String v1 = map.get(k1);
    String v2 = map.get(k2);
    String v3 = map.get(k3);

    assertEquals("firstValue", v1);
    assertEquals("secondValue", v2);
    assertEquals("thirdValue", v3);
}

执行日志:

storing value for k1
Calling hashCode()
storing value for k2
Calling hashCode()
storing value for k3
Calling hashCode()
Calling equals() for key: MyKey [name=secondKey, id=2] // 冲突时调用equals
retrieving value for k1
Calling hashCode()
retrieving value for k2
Calling hashCode()
retrieving value for k3
Calling hashCode()
Calling equals() for key: MyKey [name=secondKey, id=2] // 检索时再次调用equals

6.4 性能影响

场景 时间复杂度 原因
无冲突 O(1) 直接定位桶
链表冲突 O(n) 需遍历链表
树化冲突 O(log n) 二叉树查找效率更高

7. 总结

本文深入剖析了HashMap的核心机制:

  1. 存储原理:基于哈希表,通过键的哈希值定位桶
  2. 关键APIput()get()的执行流程与返回值规则
  3. 集合视图:键集、值集和条目集的特性与使用
  4. 性能优化:初始容量和负载因子的合理配置
  5. 冲突处理:链表与树化机制及性能影响

掌握这些原理不仅能帮助写出高效代码,也是面试必备知识。完整示例代码可在GitHub项目中获取。

💡 最后建议:实际开发中优先使用Java 19+的HashMap.newHashMap(),避免手动计算容量的坑。


原始标题:HashMap Under the Hood