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

HashMap 与 HashSet

        在 Java 集合框架中,HashMap 和 HashSet 是日常开发中出镜率极高的两个类。它们之所以受欢迎,核心在于高效的动态查找能力—— 相比传统的遍历(O (N))或二分查找(依赖有序序列),这两个集合能以接近 O (1) 的时间复杂度完成插入、删除和查找操作。。

一、哈希表

        要理解 HashMap 和 HashSet,必须先搞懂它们的底层实现 ——哈希表(Hash Table)。哈希表的本质是一种 “键 - 地址映射” 结构,通过一个「哈希函数」将关键码(Key)直接映射到存储地址,从而实现 “一次定位” 的高效查找。

1.1 哈希表的核心逻辑

        举个简单例子:我们要存储一组数字 {1,4,5,6,9,44},选择哈希函数为 hash(key) = key % 10(除留余数法,最常用的哈希函数之一),哈希表底层数组长度为 10。

  • 计算每个 key 的哈希地址:hash(1)=1hash(4)=4hash(44)=4...
  • 直接将 key 存到对应的数组索引位置:1 存到索引 1,4 存到索引 4,44 也该存到索引 4—— 这就引出了 “哈希冲突”。

1.2 哈希冲突:不可避免,但可优化

什么是哈希冲突?

        不同的 Key 通过哈希函数计算出相同的地址(比如 4 和 44 都映射到索引 4),这种现象就是哈希冲突。冲突无法完全避免(因为数组容量有限,Key 无限),但我们可以通过两种方式降低冲突影响:

1. 冲突避免:从源头减少冲突
  • 合理设计哈希函数:优先选择「除留余数法」(取一个接近数组长度的质数作为除数),保证 Key 分布均匀。
  • 控制负载因子:负载因子(负载因子 = 元素个数 / 数组长度)是哈希表的 “松紧度” 指标。负载因子越大,数组越满,冲突率越高;反之则空间浪费越多。Java 中 HashMap 的默认负载因子是 0.75—— 这是团队权衡空间与性能的结果:当元素个数超过 数组长度 * 0.75 时,就会触发「扩容」(数组长度翻倍),从而降低负载因子,减少冲突。
2. 冲突解决:哈希桶(数组 + 链表 / 红黑树)

        Java 采用「哈希桶(开散列)」解决冲突:哈希表底层是数组,每个数组元素(称为 “桶”)对应一个链表;当发生冲突时,将冲突的 Key 以链表节点的形式挂在同一个桶下。

        如果某个桶的链表过长(JDK 1.8 后阈值为 8),且数组长度 >= 64,链表会自动转为红黑树—— 因为链表查询是 O (N),而红黑树是 O (logN),能大幅提升极端场景下的性能。

public class HashBucket {//哈希桶节点(存储Key-Value)private static class Node {private int key;private int value;Node next; //链表指针(解决冲突)public Node(int key, int value) {this.key = key;this.value = value;}}private Node[] array; //底层数组(哈希桶)private int size; //当前存储的元素个数private static final double LOAD_FACTOR = 0.75; //负载因子阈值(文档1-160)private static final int DEFAULT_CAPACITY = 8; //默认初始容量//构造方法:初始化哈希桶public HashBucket() {array = new Node[DEFAULT_CAPACITY];size = 0;}/*** 插入/更新Key-Value* @param key 待插入key* @param value 待插入value* @return 若key已存在,返回旧value;否则返回-1*/public int put(int key, int value) {//1. 计算哈希地址(除留余数法,文档1-139)int index = key % array.length;//2. 检查桶中是否存在该key(存在则更新value)Node cur = array[index];while (cur != null) {if (cur.key == key) {int oldValue = cur.value;cur.value = value; //更新valuereturn oldValue;}cur = cur.next;}//3. 不存在该key,头插法插入新节点(解决冲突)Node newNode = new Node(key, value);newNode.next = array[index];array[index] = newNode;size++;//4. 检查负载因子,超过阈值则扩容if (loadFactor() >= LOAD_FACTOR) {resize();}return -1;}/*** 根据key获取value* @param key 目标key* @return 找到返回value,否则返回-1*/public int get(int key) {//1. 计算哈希地址int index = key % array.length;//2. 遍历桶中的链表查找keyNode cur = array[index];while (cur != null) {if (cur.key == key) {return cur.value;}cur = cur.next;}return -1; //未找到}/*** 计算当前负载因子*/private double loadFactor() {return size * 1.0 / array.length;}/*** 扩容:数组长度翻倍,重新哈希迁移节点*/private void resize() {//1. 创建新数组(容量翻倍)Node[] newArray = new Node[array.length * 2];//2. 遍历旧数组,迁移每个桶的节点到新数组for (int i = 0; i < array.length; i++) {Node cur = array[i];while (cur != null) {Node next = cur.next; //保存下一个节点(防止链表断裂)//重新计算当前节点在新数组中的索引int newIndex = cur.key % newArray.length;//头插法插入新数组cur.next = newArray[newIndex];newArray[newIndex] = cur;cur = next; //处理下一个节点}}//3. 替换数组引用array = newArray;}//测试方法public static void main(String[] args) {HashBucket hashBucket = new HashBucket();//插入数据(包含冲突场景:4和44哈希地址相同)hashBucket.put(4, 40);hashBucket.put(44, 440);hashBucket.put(5, 50);hashBucket.put(14, 140);//测试获取System.out.println("get(4):" + hashBucket.get(4)); //40System.out.println("get(44):" + hashBucket.get(44)); //440System.out.println("get(14):" + hashBucket.get(14)); //140System.out.println("get(20):" + hashBucket.get(20)); //-1(不存在)//测试更新hashBucket.put(4, 400);System.out.println("更新后get(4):" + hashBucket.get(4)); //400}
}

二、HashMap:键值对的高效存储方案

        HashMap 是 Map 接口的实现类,专门用于存储「Key-Value 键值对」,是开发中存储关联数据的首选。

2.1 HashMap 的核心特性

  1. Key 唯一性:同一个 HashMap 中,Key 不能重复(若插入相同 Key,会覆盖旧 Value 并返回旧值)。
  2. Value 可重复:不同 Key 可以对应相同 Value。
  3. null 允许:Key 和 Value 都可以为 null(注意:Key 最多只能有一个 null,Value 可以有多个)。
  4. 无序性:存储顺序不保证与插入顺序一致(底层哈希表是无序的)。
  5. 线程不安全:多线程环境下操作可能出现并发问题(需用 ConcurrentHashMap 替代)。

2.2 底层实现:数组 + 链表 / 红黑树

HashMap 的底层结构可以概括为 “数组承载桶,桶下挂链表 / 红黑树”:

  • 数组:称为「哈希桶数组」,每个元素是链表 / 红黑树的头节点。
  • 链表:解决哈希冲突,存储相同哈希地址的 Key-Value 对。
  • 红黑树:优化长链表的查询性能(当链表长度 > 8 且数组长度 >= 64 时转换)。

2.3 关键流程:put 与 get 是如何工作的?

1. put 方法:插入键值对

当我们调用 map.put(key, value) 时,底层会经历以下步骤:

  1. 计算 Key 的哈希值:先调用 Key 的 hashCode() 方法得到哈希值,再通过一次 “扰动计算”(减少哈希碰撞)得到最终哈希值。
  2. 确定数组索引:用最终哈希值对「当前数组长度」取模,得到 Key 对应的桶索引。
  3. 处理桶内元素
    • 若桶为空:直接新建节点插入桶中。
    • 若桶不为空:遍历链表 / 红黑树,检查是否存在相同 Key(通过 equals() 比较):
      • 存在相同 Key:更新 Value,返回旧 Value。
      • 不存在相同 Key:新建节点插入链表头部(JDK 1.8 后)或红黑树中。
  4. 检查扩容:插入后若元素个数超过 数组长度 * 0.75,触发扩容(数组长度翻倍),并重新计算所有节点的索引,迁移数据。
2. get 方法:查询键值对

调用 map.get(key) 时,流程更简单:

  1. 同 put 步骤 1-2:计算 Key 的哈希值,确定桶索引。
  2. 遍历桶内的链表 / 红黑树,通过 equals() 找到相同 Key 的节点,返回其 Value。
  3. 若未找到,返回 null。

2.4 常用方法与实战场景

核心方法
方法功能描述
V put(K key, V value)插入 / 更新键值对,返回旧 Value(若 Key 不存在则返回 null)
V get(Object key)根据 Key 获取 Value,不存在则返回 null
V remove(Object key)根据 Key 删除键值对,返回被删除的 Value(不存在则返回 null)
Set<K> keySet()返回所有 Key 的集合(用于遍历 Key)
Set<Map.Entry<K,V>> entrySet()返回所有键值对的集合(推荐用于遍历 Key-Value,效率比 keySet 高)
boolean containsKey(Object key)判断是否包含指定 Key
实战场景
  • 存储用户信息:Key 为用户 ID,Value 为 User 对象。
  • 统计单词频次:Key 为单词,Value 为出现次数(插入时判断 Key 是否存在,存在则 Value+1)。
  • 缓存临时数据:比如接口请求结果,Key 为请求参数,Value 为响应数据。

2.5 坑点提醒:自定义类作为 Key 的注意事项

        如果用自定义类(比如 User)作为 HashMap 的 Key,必须同时覆写 hashCode() 和 equals() 方法,否则会出现逻辑错误。

原因:

  • hashCode() 决定 Key 映射到哪个桶,equals() 决定桶内是否为相同 Key。
  • 若不覆写:默认使用 Object 类的方法 ——hashCode() 返回对象的内存地址,equals() 比较内存地址,导致即使内容相同的两个对象也会被视为不同 Key。

正确原则:

  • 若 a.equals(b) 为 true,则 a.hashCode() 必须等于 b.hashCode()
  • 若 a.hashCode() 相等,a.equals(b) 不一定为 true(哈希冲突)。

2.6 HashMap使用示例

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;public class HashMapDemo {public static void main(String[] args) {//1. 初始化HashMap(存储<String, Integer>:姓名-年龄)Map<String, Integer> hashMap = new HashMap<>();//2. 插入键值对(文档1-80)hashMap.put("张三", 20);hashMap.put("李四", 22);hashMap.put("王五", 21);hashMap.put("张三", 23); //重复Key,覆盖valueSystem.out.println("HashMap内容:" + hashMap); //{张三=23, 李四=22, 王五=21}//3. 常用方法//获取valueSystem.out.println("张三的年龄:" + hashMap.get("张三")); //23System.out.println("赵六的年龄(默认值):" + hashMap.getOrDefault("赵六", 0)); //0(不存在)//判断是否包含Key/ValueSystem.out.println("是否包含Key李四:" + hashMap.containsKey("李四")); //trueSystem.out.println("是否包含Value21:" + hashMap.containsValue(21)); //true//遍历方式1:遍历所有Key(keySet)System.out.print("遍历Key:");Set<String> keySet = hashMap.keySet();for (String key : keySet) {System.out.print(key + " "); //张三 李四 王五(无序)}System.out.println();//遍历方式2:遍历所有Value(values)System.out.print("遍历Value:");Collection<Integer> values = hashMap.values();for (Integer value : values) {System.out.print(value + " "); // 23 22 21}System.out.println();//遍历方式3:遍历键值对(entrySet,推荐高效)System.out.println("遍历键值对:");Set<Map.Entry<String, Integer>> entrySet = hashMap.entrySet();for (Map.Entry<String, Integer> entry : entrySet) {System.out.println(entry.getKey() + "->" + entry.getValue());}//删除KeyhashMap.remove("王五");System.out.println("删除王五后:" + hashMap); //{张三=23, 李四=22}}
}

三、HashSet:一键去重的 “利器”

        HashSet 是 Set 接口的实现类,专门用于存储 “不重复的单个元素”,核心功能是去重

3.1 HashSet 的核心特性

  1. 元素唯一性:集合中不会有重复元素(add 重复元素会返回 false)。
  2. 只存 Key:相比 HashMap,HashSet 只存储单个元素(本质是 HashMap 的 Key)。
  3. null 允许:最多只能有一个 null 元素。
  4. 无序性:存储顺序与插入顺序无关。
  5. 线程不安全:与 HashMap 一致,多线程需谨慎。

3.2 底层秘密:基于 HashMap 实现

        很多人不知道,HashSet 其实是 “借壳” HashMap 实现的 —— 它的底层就是一个 HashMap!当我们调用 hashSet.add(element) 时,底层实际执行的是:hashMap.put(element, PRESENT)其中 PRESENT 是一个静态的空 Object 对象(private static final Object PRESENT = new Object()),仅作为占位符使用(因为 HashMap 需要键值对,而 HashSet 只需要 Key)。

        这就解释了为什么 HashSet 能去重:因为 HashMap 的 Key 是唯一的,HashSet 的元素本质就是 HashMap 的 Key。

3.3 常用方法与去重原理

核心方法
方法功能描述
boolean add(E e)添加元素,成功返回 true,重复返回 false
boolean contains(Object o)判断集合是否包含元素 o,存在返回 true
boolean remove(Object o)删除元素 o,成功返回 true,不存在返回 false
int size()返回集合中元素的个数
void clear()清空集合所有元素
去重原理

HashSet 的去重逻辑完全依赖 HashMap 的 Key 唯一性:

  1. 调用元素的 hashCode() 计算哈希值,确定 HashMap 的桶索引。
  2. 遍历桶内元素,调用 equals() 比较是否存在相同元素。
  3. 若存在,add 方法返回 false,不插入;若不存在,插入元素(作为 HashMap 的 Key),返回 true。

因此,自定义类作为 HashSet 元素时,同样需要覆写 hashCode() 和 equals() 方法,否则无法正确去重。

3.4 实战场景

  • 列表去重:比如将 List<String> phones 转为 HashSet,快速去除重复手机号。
  • 判断元素存在:比如检查某个用户 ID 是否在 “黑名单” 集合中。
  • 统计唯一值:比如统计日志中出现的唯一 IP 地址。

3.5 自定义类作为 HashMap/HashSet Key(需覆写 hashCode () 和 equals ())

import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;//自定义类:User(作为Key)
class User {private String id;private String name;public User(String id, String name) {this.id = id;this.name = name;}//自定义Key必须覆写equals()和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) && Objects.equals(name, user.name);}@Overridepublic int hashCode() {return Objects.hash(id, name);}@Overridepublic String toString() {return "User{id='" + id + "', name='" + name + "'}";}
}//测试自定义Key
public class CustomKeyDemo {public static void main(String[] args) {//1. 自定义Key在HashMap中的使用HashMap<User, String> userMap = new HashMap<>();User user1 = new User("001", "张三");User user2 = new User("001", "张三"); //与user1内容相同,应视为同一KeyuserMap.put(user1, "北京");userMap.put(user2, "上海"); //覆盖user1的valueSystem.out.println("HashMap中User对应的地址:" + userMap.get(user1)); // 上海(证明user1和user2是同一Key)//2. 自定义Key在HashSet中的使用HashSet<User> userSet = new HashSet<>();userSet.add(user1);userSet.add(user2); //重复Key,无法插入System.out.println("HashSet大小:" + userSet.size()); //1(去重成功)System.out.println("HashSet内容:" + userSet); //[User{id='001', name='张三'}]}
}

四、横向对比:理清这些 “Set/Map” 的区别

        实际开发中,我们常需要在 HashMap/HashSet 和 TreeMap/TreeSet 之间做选择,这里用表格清晰对比:

4.1 HashMap vs HashSet

对比维度HashMapHashSet
存储内容键值对(K-V)单个元素(K)
底层依赖直接基于哈希表基于 HashMap(Value 占位)
核心功能关联数据存储元素去重
关键方法put、getadd、contains
null 允许度Key 和 Value 均可为 null仅元素(K)可为 null

4.2 HashMap/HashSet vs TreeMap/TreeSet

对比维度HashMap/HashSetTreeMap/TreeSet
底层结构数组 + 链表 / 红黑树红黑树
有序性无序(插入顺序不保证)有序(Key 自然排序 / 定制排序)
时间复杂度插入 / 查找 / 删除 O (1)插入 / 查找 / 删除 O (logN)
Key 要求需覆写 hashCode + equals需实现 Comparable 或提供 Comparator
null 允许度允许(HashMap Key 可 null)不允许(TreeMap Key 不可 null)
适用场景追求高效读写,无需有序需要按 Key 排序的场景

总结

  1. 底层基石是哈希表:HashMap 和 HashSet 都基于哈希表实现,高效性的核心是 “哈希函数映射 + 冲突解决”。
  2. HashMap 是键值对存储:Key 唯一,Value 可重复,需注意自定义 Key 覆写 hashCode 和 equals。
  3. HashSet 是去重工具:底层依赖 HashMap,元素即 HashMap 的 Key,去重逻辑与 HashMap Key 唯一性一致。
  4. 选择依据看需求:需有序用 Tree 系列,需高效用 Hash 系列;存键值对用 Map,存单个元素用 Set。

掌握这些核心逻辑后,再遇到 HashMap 或 HashSet 的问题(比如为什么会有重复元素、为什么查询慢),就能从底层原理出发快速定位原因,真正做到 “知其然,更知其所以然”。

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

相关文章:

  • 怎么在虚拟主机上建网站wordpress rest图片
  • 小米手机之间数据转移的6种方法
  • 前端开发中的表格标签
  • PaddleOCR-VL本地部署流程
  • 2.2 复合类型
  • 做网站图片自动切换宁波软件开发
  • quat:高性能四元数运算库
  • [MySQL]表——分组查询
  • 济南做网站的好公司有哪些极简资讯网站开发
  • 网站后台页面设计互联网+可以做什么项目
  • 项目八 使用postman实现简易防火墙功能
  • 使用postman 测试restful接口
  • 2008 iis 添加 网站 权限设置网站策划案4500
  • 以自主创新推动能源装备智能化升级,为能源安全构筑“确定性”底座
  • 构建AI智能体:七十六、深入浅出LoRA:低成本高效微调大模型的原理与实践
  • 中国各大网站排名网站源码爬取
  • FFmpeg 安装与配置教程(Windows 系统)
  • 【数字逻辑】 60进制数字秒表设计实战:用74HC161搭计数器+共阴极数码管显示(附完整接线图)
  • 新网网站空间花都网站建设价格
  • 前端底层原理与复杂问题映射表
  • Digital Micrograph下载安装教程
  • 怎么做网站的301建设设计院网站
  • 自己的服务器 linux centos8部署django项目
  • 做网站注册会员加入实名认证功能广西建设工程质量监督网站
  • 遵义哪里有做网站的外国网站上做Task
  • 动态修改浏览器地址而不刷新页面
  • 车牌识别相机:中哈口岸的通关智能助力
  • 音视频开发远端未发布视频占位图2
  • 【普中STM32F1xx开发攻略--标准库版】-- 第 9 章 STM32 固件库介绍
  • OpenCV 视频处理