HashSet的唯一性由HashMap键唯一性及对象的hashCode()和equals()协同保障:先通过hashCode()定位桶,再用equals()比对同桶元素;二者必须同时重写且逻辑一致,否则破坏唯一性或性能;null允许且仅存一个;底层实为HashMap.put(key, PRESENT)。

HashSet 的“唯一性”不是靠人工检查或逐个比对实现的,而是由底层 HashMap 的键唯一机制 + 对象自身的 hashCode() 和 equals() 协同保障的。理解它,关键在搞清“什么时候算重复”。
唯一性的判断分两步走
往 HashSet 添加一个元素时,JVM 不会直接遍历全部已有元素去比较,而是按固定流程快速决策:
- 先调用该元素的 hashCode() 方法,算出一个整数哈希值
- 用这个哈希值决定它该放在底层哈希表的哪个“桶”(bucket)里
- 如果那个桶里已经存在其他元素,就逐个调用 equals() 方法,和新元素做比较
- 只要发现有一个已存在元素满足:hashCode() 相同 且 equals() 返回 true,就认定为重复,不添加
为什么必须同时重写 hashCode() 和 equals()?
这是最容易出错的地方。如果只重写 equals() 而不重写 hashCode(),两个逻辑上相等的对象可能被分配到不同桶中,导致 HashSet 认为它们不重复,实际存了两份;反之,只重写 hashCode() 会导致所有对象挤在一个桶里,退化成链表遍历,性能暴跌。
- 重写规则要一致:equals() 判定为 true 的两个对象,hashCode() 必须返回相同值
- 推荐用 IDE 自动生成(如 IntelliJ 的 Alt+Insert → “Generate hashCode() and equals()”),避免手写疏漏
- 对于 String、Integer 等 JDK 内置类,已经正确实现了这两个方法,可直接放心用
null 元素也遵守唯一性规则
HashSet 允许存一个 null,但只能有一个。它的处理是特例:
立即学习“Java免费学习笔记(深入)”;
- null 的 hashCode() 规定为 0,会被放到哈希表索引为 0 的桶中
- 插入 null 时,会检查该桶中是否已有 null —— 是则跳过,否则放入
- 所以第二次 add(null) 不会生效,这也符合“唯一性”的定义
底层其实是 HashMap 的“伪装”
你看到的 HashSet.add("abc"),本质上执行的是:
map.put("abc", PRESENT);其中 PRESENT 是一个共享的空 Object 实例。HashMap 的 key 天然不允许重复,HashSet 就借用了这个特性——把你要存的数据全当 key,value 统一占位。所以“唯一性”本质就是“HashMap 键的唯一性”。


















