hashset底层用hashmap的key存元素,因key天然不重复且增查平均o(1);其add()即map.put(e, present),去重逻辑完全由hashmap的hashcode()和equals()决定。

HashSet 底层为什么用 HashMap 的 key 来存元素
因为 HashMap 的 key 天然不允许重复,且查找、插入平均时间复杂度是 O(1),HashSet 直接复用这套机制,自己不额外维护去重逻辑——它只是把所有元素都塞进一个 HashMap 的 key 位置,value 固定用一个叫 PRESENT 的静态 Object 占位。
这不是“模拟”或“借鉴”,而是真实继承关系之外的组合复用:JDK 源码里 HashSet 内部就持有一个 HashMap 实例,add() 方法本质就是调用 map.put(e, PRESENT)。
-
HashSet的add()成功返回true,仅当HashMap.put()返回null(即旧值不存在) - 元素的
hashCode()和equals()全部由HashMap在 put 时调用,HashSet自己不重写任何判断逻辑 - 所以如果你自定义类放进
HashSet却没去重,问题一定出在没正确重写hashCode()和equals(),而不是HashSet本身有问题
new HashSet() 时传 initialCapacity 会影响 HashMap 的什么
它直接传给内部 HashMap 的构造函数,控制的是 HashMap 底层数组的初始容量,不是 HashSet 自己的容量——HashSet 根本没有独立的存储结构。
传错值的后果很实际:如果预估要放 1000 个元素,却用默认构造(initialCapacity=16),HashMap 会反复扩容 5 次(16→32→64→128→256→512→1024),每次扩容都要 rehash 所有已有 key,性能掉得明显。
- 推荐按预期元素数 ÷ 0.75(即负载因子)向上取整,比如 1000 个元素,设
initialCapacity = 1334或直接1000 / 0.75 + 1 - 传 0 不会报错,但
HashMap会自动设为 1;传负数会抛IllegalArgumentException - 这个参数只影响初始化时的数组大小,不影响后续动态扩容行为
HashSet.contains() 为什么有时返回 false,明明元素看着一样
典型现象:两个内容相同的字符串或自定义对象,set.contains(obj) 返回 false,但 System.out.println(set) 又能打印出来。
根本原因只有两个:hashCode() 不一致,或者 equals() 没覆盖、逻辑错、没遵守对称性/传递性等约定。因为 contains() 先算 obj.hashCode() % table.length 定位桶,再遍历桶内链表或红黑树,逐个调 node.key.equals(obj) 判断。
- 如果
obj.hashCode()和集合里那个对象的hashCode()不同,连桶都进不去,直接返回false - 如果
hashCode()相同但equals()返回false(比如只比较了部分字段),也会返回false - String、Integer 等 JDK 类没问题,但你自己写的类,只要没用 IDE 自动生成
hashCode()/equals(),大概率踩坑
HashSet 的迭代顺序和 HashMap 的迭代顺序一致吗
一致,但都不保证有序——它们都依赖 HashMap 的底层实现:JDK 8+ 中,HashMap 迭代顺序是“桶数组从左到右,每个桶内按插入顺序(链表)或树中序(红黑树)”,但这只是当前实现细节,不是规范承诺。
也就是说:HashSet 的 iterator() 返回的顺序,就是其内部 HashMap 的 keySet 迭代顺序,而这个顺序既不按插入顺序(LinkedHashSet 才保证),也不按自然顺序(TreeSet 才保证),更不按哈希值大小。
- 不要在代码里依赖
HashSet的遍历顺序,哪怕测试时看起来稳定,换个 JDK 版本或元素数量就可能变 - 如果需要可预测顺序,明确选
LinkedHashSet(插入序)或TreeSet(排序序) -
HashSet的迭代性能略低于ArrayList,因为要跳指针、处理哈希冲突,但这是换去重能力的合理代价
真正容易被忽略的点是:你改了一个类的 hashCode() 实现,又把它放进 HashSet 后再修改字段,这个对象很可能再也找不到了——因为 hash 值变了,但位置没更新,它就永远卡在旧桶里。










