HashSet去重严格按“先hashCode()后equals()”两步执行:桶空则只调hashCode(),桶非空才依次调equals();仅重写equals()不重写hashCode()会导致去重失效,因默认哈希值基于内存地址,相同内容对象被分至不同桶,equals()永不触发。
存入HashSet时,先调用
,再可能调用
HashSet判断一个新元素是否已存在,严格按两步走:先算
,再决定要不要调用
。不是“两个都调”,也不是“随机选一个”,而是有明确的短路逻辑。
具体流程是:
返回的值决定了该对象落在哈希表哪个“桶”(bucket)里;如果那个桶里当前为空,直接插入,
根本不会被触发;只有桶里已有至少一个对象,才会拿新对象和桶内每个对象依次调用
比对。
桶为空 → 只调
,不调
桶非空 → 先调
定位桶,再对桶内每个对象调
不同 → 绝对不调
,哪怕两个对象内容完全一样
为什么只重写
却无法去重
因为
没重写时,默认继承自
,返回的是内存地址的哈希值。两个内容相同的自定义对象,只要不是同一个实例(
为
),
就大概率不同 —— 这导致它们被分到不同桶里,
压根没机会运行。
常见错误现象:
加了两次进
,结果集合大小是2,而不是1。打印调试会发现
一次都没被调用过。
重写
但不重写
→ 去重失效,且无任何报错或警告
IDE(如IntelliJ)在重写
时通常会提示“also override
”,这不是建议,是契约
用
生成
最安全,它自动处理
和浮点数比较等边界
相同但
返回
是正常现象
哈希冲突不可避免。比如两个
对象,一个叫"Bob"年龄25,另一个叫"Tom"年龄25,若
只基于
计算,它们哈希值就相同,但
会因
不同而返回
。这时HashSet会把它们都存进同一个桶,用链表或红黑树组织。
这种设计是权衡:用空间换时间。哈希码负责快速过滤掉99%的不等对象;
兜底处理剩下那1%的冲突。
哈希值相同 ≠ 对象相等,只是“需要进一步确认”的信号
过度追求
分散(比如加入随机因子)反而破坏一致性,导致
查不到自己刚
的对象
一旦
依赖可变字段(如
后续被
修改),对象存进HashSet后就再也找不到了
自定义类放进HashSet前必须检查的三件事
不是“写了
和
就行”,得看它们是否真正协同工作。最容易忽略的是语义一致性 —— 什么情况下你认为两个对象“应该算重复”?这个业务逻辑必须同时体现在两个方法里。
确认
中用的字段,和
里
传的字段完全一致
验证交换顺序是否影响结果:比如
和
应视为相同,则
不能简单写成
,得用对称写法(如排序后哈希)
字段含
类型时,避免直接用
比对,在
里用
,在
里用
而非
实际开发中,最常踩的坑不是不会写,而是改了业务逻辑后忘了同步更新其中一个方法。只要
和
对同一组字段做同构运算,且不引入可变状态,HashSet的去重行为就是可预测的。
hashCode()equals()hashCode()equals()hashCode()equals()equals()hashCode()equals()hashCode()equals()hashCode()equals()equals()hashCode()Object==falsehashCode()equals()new Person("Alice", 30)HashSetequals()equals()hashCode()equals()hashCode()Objects.hash(...)hashCode()nullhashCode()equals()falsePersonhashCode()ageequals()namefalseequals()hashCode()contains()add()hashCode()namesetName()equalshashCodeequals()hashCode()Objects.hash(...)Comparison(se1, se2)Comparison(se2, se1)hashCode()Objects.hash(se1, se2)double==equals()Double.compare()hashCode()Double.hashCode()(int) valuehashCode()equals()