hashset靠hashcode()快速定位桶、再用equals()在桶内确认重复;必须同时重写二者且逻辑一致,string已实现故可直接去重,自定义类需手动重写,否则因默认地址哈希导致重复。

HashSet底层怎么靠哈希值和equals判断重复
HashSet不存重复元素,不是靠“记住所有旧值挨个比对”,而是两步:先算hashCode()快速分流到桶(bucket),再在同一个桶里用equals()逐个确认。如果两个对象hashCode()不同,直接不算重复;如果相同,才触发equals()比较。
常见错误现象:new Person("Alice", 25)和new Person("Alice", 25)放进HashSet却出现两个——因为没重写hashCode()和equals(),默认用Object的实现,每个实例地址不同,哈希值就不同,equals也返回false。
- 必须同时重写
hashCode()和equals(),且逻辑一致:如果equals()返回true,hashCode()必须相同 - 字段选型要稳定:别用可变字段(比如一个会改的
name字段参与hash计算,之后改了会导致查不到) - IDE生成的
hashCode()通常够用,但要注意是否包含所有用于判等的关键字段
Java中add()方法返回false说明什么
HashSet.add()返回boolean:成功添加返回true,已存在则返回false。这不是异常,是正常流程反馈,常被误当成错误处理。
使用场景:比如去重导入用户数据时,用返回值统计“实际新增人数”;或配合if (!set.add(item)) { log.warn("重复项: " + item); }做轻量级重复监控。
- 别把
false当异常抛出,除非业务明确要求“禁止任何重复”并中断流程 - 注意并发场景:
HashSet本身不线程安全,多线程调用add()可能漏判重复,此时应换ConcurrentHashMap.newKeySet()或加锁 - 性能上,
add()平均时间复杂度O(1),但极端哈希碰撞(所有元素挤进一个桶)会退化成O(n)
为什么String能直接当HashSet元素去重,而自定义类不行
String类已经重写了hashCode()和equals(),且契约严谨:内容相同则哈希值相同、equals为true。所以new HashSet(Arrays.asList("a", "a"))自然只剩一个"a"。
自定义类默认继承Object,hashCode()基于内存地址,equals()也是地址比较——两个“看起来一样”的对象,在HashSet眼里完全是陌生人。
- 验证方式:打印
obj.hashCode(),两个新构造的同值对象,如果哈希值不同,就还没正确重写 - 不要只重写
equals()忽略hashCode(),否则可能进错桶,导致“明明该去重却没去重” - 用Lombok的话,
@EqualsAndHashCode要显式指定include字段,避免漏掉关键判等属性
HashSet去重后遍历顺序是随机的吗
是的,HashSet不保证迭代顺序,每次运行结果都可能不同。它底层是哈希表+链表(JDK 8起是链表或红黑树),插入顺序和哈希分布共同决定遍历次序,不是按添加先后,也不是按哈希值大小。
如果你需要“去重 + 保持插入顺序”,得换LinkedHashSet;如果需要“去重 + 按自然顺序或自定义排序”,得用TreeSet(但注意TreeSet用的是compareTo()或Comparator,不是hashCode()/equals())。
-
LinkedHashSet开销略大(维护双向链表),但去重+顺序需求强烈时值得 - 别试图通过反复创建HashSet来“撞出固定顺序”,那是不可靠的副作用
- 测试时如果发现顺序偶然稳定,不代表生产环境也稳定——哈希算法细节可能随JDK版本微调
hashCode()和equals()时字段选择的隐蔽性:比如一个User类,用id判等就够了,但有人顺手把lastLoginTime也塞进hashCode(),结果时间一变,对象在Set里就找不到了。










