当前位置: 首页 > news >正文

数据结构-HashMap

在 Java 键值对(Key-Value)集合中,HashMap 是使用频率最高的实现类之一,凭借高效的查找、插入性能,成为日常开发的 “利器”。本文将从 HashMap 的底层原理、核心特点、常用方法到遍历方式、使用注意事项,进行系统性梳理,帮助快速掌握其核心逻辑与实战技巧。

一、HashMap 核心认知:底层原理与特点

HashMap 本质是哈希表(数组 + 链表 / 红黑树) 实现的键值对存储结构,核心目标是通过 “哈希算法” 快速定位元素,平衡查询与增删效率。其核心特点如下:

1. 底层存储结构(JDK 1.8+)

  • 基础结构:数组(称为 “哈希桶”)+ 链表 + 红黑树。
    • 数组:每个元素是一个 “链表头节点”,通过 key 的哈希值计算数组索引(index = (数组长度 - 1) & 哈希值),实现快速定位。
    • 链表:当多个 key 计算出相同索引(哈希冲突)时,元素以链表形式存储在对应桶中。
    • 红黑树:当链表长度超过 8 且数组长度 ≥ 64 时,链表会转为红黑树,将查询时间复杂度从 O (n) 优化为 O (log n)(避免链表过长导致性能下降)。
  • 示意图简化理解
数组索引 0:[Node(key1, val1)] → [Node(key2, val2)]  // 链表(长度<8)
数组索引 1:[TreeNode(key3, val3)] → 红黑树结构       // 红黑树(长度≥8)
数组索引 2:null
...

2. 核心特点

  1. 键值对规则
    • Key 唯一:若添加重复 Key,新 Value 会覆盖旧 Value(put() 方法返回旧 Value)。
    • Value 可重复:不同 Key 可对应相同 Value。
    • 允许 null:Key 最多允许 1 个 null(重复添加 null Key 会覆盖),Value 可多个 null。
  2. 无序性:存储顺序与插入顺序无关(底层按哈希值排序,非插入顺序)。
  3. 线程不安全:非同步设计,多线程同时读写可能出现数据异常(如 ConcurrentModificationException),需手动处理线程安全(如 Collections.synchronizedMap() 或 ConcurrentHashMap)。
  4. 自动扩容
    • 默认初始容量:16(数组长度,必须是 2 的幂,确保哈希计算均匀)。
    • 负载因子:默认 0.75(当元素数量 ≥ 容量 × 负载因子时触发扩容)。
    • 扩容规则:新容量 = 旧容量 × 2,同时重新计算所有元素的哈希索引(“rehash”),会消耗一定性能。
  5. 高效性能
    • 理想情况下,插入、查询、删除的时间复杂度均为 O(1)(直接通过哈希值定位桶)。
    • 哈希冲突较少时,性能接近理想值;冲突严重(链表过长)时,性能会下降(红黑树优化可缓解此问题)。

二、HashMap 常用方法

HashMap 提供了丰富的方法操作键值对,以下是开发中最常用的方法,均附完整示例代码,可直接复制运行。

1. 基础操作:添加、获取、删除

(1)添加键值对(put ())
  • V put(K key, V value):添加键值对,若 Key 已存在则覆盖 Value,返回旧 Value(若 Key 不存在则返回 null)。
  • void putAll(Map<? extends K, ? extends V> m):将另一个同类型 Map 的所有键值对添加到当前 HashMap 中(重复 Key 会被覆盖)。
import java.util.HashMap;public class HashMapPutDemo {public static void main(String[] args) {// 1. 单个键值对添加HashMap<String, Integer> scoreMap = new HashMap<>();Integer oldMathScore = scoreMap.put("数学", 90); // Key 不存在,返回 nullSystem.out.println("旧数学成绩:" + oldMathScore); // 输出:nullscoreMap.put("语文", 85);scoreMap.put("英语", 95);System.out.println("添加后:" + scoreMap); // 输出:{数学=90, 语文=85, 英语=95}// 重复 Key 覆盖:数学成绩从 90 改为 98Integer updatedOldScore = scoreMap.put("数学", 98);System.out.println("被覆盖的旧数学成绩:" + updatedOldScore); // 输出:90System.out.println("覆盖后:" + scoreMap); // 输出:{数学=98, 语文=85, 英语=95}// 2. 批量添加(putAll())HashMap<String, Integer> extraScoreMap = new HashMap<>();extraScoreMap.put("物理", 88);extraScoreMap.put("化学", 92);scoreMap.putAll(extraScoreMap);System.out.println("批量添加后:" + scoreMap); // 输出:{数学=98, 语文=85, 英语=95, 物理=88, 化学=92}}
}
(2)获取值与判断存在(get ()、containsKey ()、containsValue ())
  • V get(Object key):根据 Key 获取 Value,若 Key 不存在则返回 null(注意:若 Value 本身是 null,需用 containsKey() 区分 “Key 不存在” 和 “Value 为 null”)。
  • boolean containsKey(Object key):判断 HashMap 是否包含指定 Key,返回布尔值。
  • boolean containsValue(Object value):判断 HashMap 是否包含指定 Value,返回布尔值。
public class HashMapGetContainsDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("生物", null); // Value 为 null// 1. 根据 Key 获取 ValueInteger mathScore = scoreMap.get("数学");Integer historyScore = scoreMap.get("历史"); // Key 不存在Integer bioScore = scoreMap.get("生物"); // Value 本身是 nullSystem.out.println("数学成绩:" + mathScore); // 输出:98System.out.println("历史成绩(Key 不存在):" + historyScore); // 输出:nullSystem.out.println("生物成绩(Value 为 null):" + bioScore); // 输出:null// 2. 判断 Key 是否存在(区分“Key 不存在”和“Value 为 null”)boolean hasBioKey = scoreMap.containsKey("生物");boolean hasHistoryKey = scoreMap.containsKey("历史");System.out.println("是否包含 Key '生物':" + hasBioKey); // 输出:trueSystem.out.println("是否包含 Key '历史':" + hasHistoryKey); // 输出:false// 3. 判断 Value 是否存在boolean has98 = scoreMap.containsValue(98);boolean has100 = scoreMap.containsValue(100);System.out.println("是否包含 Value 98:" + has98); // 输出:trueSystem.out.println("是否包含 Value 100:" + has100); // 输出:false}
}
(3)删除键值对(remove ())
  • V remove(Object key):根据 Key 删除键值对,返回被删除的 Value(若 Key 不存在则返回 null)。
public class HashMapRemoveDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 95);// 删除 Key 为“语文”的键值对Integer removedChineseScore = scoreMap.remove("语文");System.out.println("被删除的语文成绩:" + removedChineseScore); // 输出:85System.out.println("删除后:" + scoreMap); // 输出:{数学=98, 英语=95}// 删除不存在的 KeyInteger removedHistoryScore = scoreMap.remove("历史");System.out.println("删除不存在的 Key 返回值:" + removedHistoryScore); // 输出:null}
}

2. 进阶操作:修改、清空、判断空否

(1)修改 Value(replace ())
  • V replace(K key, V value):仅当 Key 存在时,用新 Value 替换旧 Value,返回旧 Value(若 Key 不存在则返回 null,区别于 put()put() 会新增不存在的 Key)。
public class HashMapReplaceDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("英语", 95);// 修改存在的 Key(英语成绩从 95 改为 97)Integer oldEnglishScore = scoreMap.replace("英语", 97);System.out.println("被修改的旧英语成绩:" + oldEnglishScore); // 输出:95System.out.println("修改后:" + scoreMap); // 输出:{数学=98, 英语=97}// 修改不存在的 Key(不会新增,返回 null)Integer oldHistoryScore = scoreMap.replace("历史", 80);System.out.println("修改不存在的 Key 返回值:" + oldHistoryScore); // 输出:nullSystem.out.println("修改后集合:" + scoreMap); // 输出:{数学=98, 英语=97}(无变化)}
}
(2)清空与判断空否(clear ()、isEmpty ()、size ())
  • void clear():清空 HashMap 中所有键值对(集合变为空,对象本身仍存在)。
  • boolean isEmpty():判断 HashMap 是否为空(元素个数为 0),返回布尔值。
  • int size():返回 HashMap 中键值对的实际个数(区别于 “容量”)。
public class HashMapClearEmptySizeDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("英语", 97);// 1. 获取集合大小System.out.println("初始元素个数:" + scoreMap.size()); // 输出:2// 2. 判断是否为空System.out.println("初始是否为空:" + scoreMap.isEmpty()); // 输出:false// 3. 清空集合scoreMap.clear();System.out.println("清空后元素个数:" + scoreMap.size()); // 输出:0System.out.println("清空后是否为空:" + scoreMap.isEmpty()); // 输出:true}
}

3. HashMap 三种核心遍历方式

HashMap 存储的是 “键值对(Entry)”,遍历需围绕 “Key 集合”“Value 集合”“Entry 集合” 展开,三种常用方式如下:

(1)遍历 Key 集合,再获取 Value(keySet ())

通过 keySet() 获取所有 Key 的集合,遍历 Key 后用 get(key) 获取对应 Value,适合仅需 Key 或需通过 Key 处理 Value 的场景。

public class HashMapKeySetDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 97);// 遍历 Key 集合for (String subject : scoreMap.keySet()) {Integer score = scoreMap.get(subject);System.out.println(subject + ":" + score);}// 输出(顺序不固定):// 数学:98// 语文:85// 英语:97}
}
(2)直接遍历 Entry 集合(entrySet ())

通过 entrySet() 获取所有键值对(Map.Entry<K, V>)的集合,直接获取 Key 和 Value,效率最高(无需二次 get(key) 查询),是开发首选。

public class HashMapEntrySetDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 97);// 遍历 Entry 集合(推荐)for (HashMap.Entry<String, Integer> entry : scoreMap.entrySet()) {String subject = entry.getKey(); // 获取 KeyInteger score = entry.getValue(); // 获取 ValueSystem.out.println(subject + ":" + score);}// 输出(顺序不固定):// 数学:98// 语文:85// 英语:97}
}
(3)遍历 Value 集合(values ())

通过 values() 获取所有 Value 的集合,仅遍历 Value,适合无需 Key、仅需处理 Value 的场景(无法通过 Value 反向获取 Key)。

public class HashMapValuesDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 97);// 遍历 Value 集合System.out.println("所有成绩:");for (Integer score : scoreMap.values()) {System.out.println(score);}// 输出(顺序不固定):// 98// 85// 97}
}

三、HashMap 使用注意事项

  1. Key 的选择原则

    • Key 必须重写 hashCode() 和 equals() 方法(否则无法正确判断 Key 唯一性,导致哈希冲突无法解决)。
    • 推荐使用不可变类作为 Key(如 StringInteger):若 Key 是可变对象,修改后哈希值变化,会导致无法通过原 Key 获取 Value。
    • 示例:若用 User 类作为 Key,需手动重写方法:
class User {private String id;// 重写 hashCode() 和 equals()@Overridepublic int hashCode() { return id.hashCode(); }@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;User user = (User) o;return Objects.equals(id, user.id);}
}
  1. null 键值的注意事项

    • Key 最多 1 个 null,重复添加会覆盖;Value 可多个 null。
    • 用 get(key) 获取 Value 时,若返回 null,需通过 containsKey(key) 确认是 “Key 不存在” 还是 “Value 为 null”。
  2. 线程安全问题

    • 单线程环境:直接使用 HashMap 即可。
    • 多线程环境:
      • 若需弱一致性:使用 Collections.synchronizedMap(new HashMap<>())(对整个 HashMap 加锁,性能较低)。
      • 若需高性能:优先使用 ConcurrentHashMap(JDK 1.8+ 采用分段锁,性能优于同步 HashMap)。
  3. 性能优化技巧

    • 初始容量指定:若已知元素数量,创建时指定初始容量(如 new HashMap<>(100)),避免频繁扩容(扩容需 rehash,消耗性能)。
    • 负载因子调整:默认 0.75 是 “性能与空间” 的平衡,若内存充足可降低(如 0.5,减少哈希冲突),若内存紧张可提高(如 0.8,减少数组占用空间)。
    • 避免哈希冲突:合理重写 Key 的 hashCode() 方法,尽量让哈希值均匀分布,减少链表 / 红黑树的长度。

与 TreeMap/Hashtable 的区别(避免混淆)

特性HashMapTreeMapHashtable(不推荐)
排序无序(按哈希值)有序(Key 自然排序 / 自定义排序)无序
线程安全非线程安全非线程安全线程安全(全方法同步)
null 允许Key 1 个 null,Value 多个 null不允许 null不允许 null
底层结构数组 + 链表 / 红黑树红黑树数组 + 链表
适用场景通用高效查询需要有序键值对遗留多线程场景(已被 ConcurrentHashMap 替代)

四、总结

HashMap 是 Java 键值对集合的核心实现,核心优势在于 “哈希表” 带来的 O (1) 高效性能,适合大多数无需有序、单线程的键值对存储场景。掌握其底层结构(数组 + 链表 / 红黑树)、常用方法(put/get/remove/ 遍历)及使用注意事项(Key 重写方法、线程安全、性能优化),就能在开发中灵活应对各类场景。

http://www.dtcms.com/a/343224.html

相关文章:

  • Kubernetes“城市规划”指南:告别资源拥堵与预算超支,打造高效云原生都市
  • Typora 快速使用入门:15分钟掌握高效写作
  • 锅炉铸造件三维扫描尺寸及形位公差检测技术方案-中科米堆CASAIM
  • ⸢ 啟 ⸥ ⤳ 为什么要开这个专栏?
  • Ubuntu Server 系统安装 Docker
  • uni-app:实现文本框的自动换行
  • SpringBoot + Vue实现批量导入导出功能的标准方案
  • k8sday13数据存储(1.5/2)
  • 基于Matlab多技术融合的红外图像增强方法研究
  • C++---滑动窗口平滑数据
  • 瑞派亚宠展专访 | 以数智化重塑就医体验,共筑宠物健康新生态
  • 区块链存证操作
  • echarts关系图(Vue3)节点背景图连线设置
  • 2025.7.19卡码刷题-回溯算法-组合
  • IOS购买订阅通知信息解析说明Java
  • 设计模式3-模板方法模式
  • 爬虫基础学习-项目实践:每次请求,跟换不同的user-agent
  • 茶饮业内卷破局,从人力管理入手
  • iOS 手势与控件事件冲突解决清单
  • 一本通1342:【例4-1】最短路径问题
  • 【Docker基础】Docker-Compose核心配置文件深度解析:从YAML语法到高级配置
  • 一个状态机如何启动/停止另一个状态机
  • C++ 常见的排序算法详解
  • CPP学习之priority_queue的使用及模拟实现
  • 3维模型导入到3Dmax中的修改色彩简单用法----第二讲
  • Kotlin 中适用集合数据的高阶函数(forEach、map、filter、groudBy、fold、sortedBy)
  • AI客服系统架构与实现:大模型、知识库与多轮对话的最佳实践
  • 蛋白质分析常用数据库2
  • QT开发---QT布局与QSS样式设置
  • 网络打印机自动化部署脚本