260. Java 集合 - 深入了解 HashSet 的内部结构

260. Java 集合 - 深入了解 HashSet 的内部结构

🧠 为什么要单独讲 HashSet

虽然 HashSet 看起来只是一个用来保存不重复元素的集合类,但它的内部实现其实是基于 HashMap 的。

👇来看一下它的核心结构:

java 复制代码
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

📌 结论一:

HashSet 中,你添加的对象其实是被作为 HashMap 的 key 存储的。

  • 所以:HashSet 的元素必须有稳定的 hashCode()equals() 行为。
  • 所以:一旦你修改了已放入 HashSet 中对象的"关键属性",你就可能遇到查找失败、数据重复、集合错乱等奇怪问题。

🧪 示例:向 HashSet 中添加可变对象

我们复用之前的 Key 类,它是一个有 setKey() 方法的可变类

java 复制代码
class Key {
    private String key;

    public Key(String key) {
        this.key = key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    @Override
    public String toString() {
        return key;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Key)) return false;
        Key other = (Key) o;
        return Objects.equals(this.key, other.key);
    }

    @Override
    public int hashCode() {
        return key.hashCode();
    }
}

🧪 添加元素并修改 key 值:

java 复制代码
Key one = new Key("1");
Key two = new Key("2");

Set<Key> set = new HashSet<>();
set.add(one);
set.add(two);

System.out.println("set = " + set);

// ❌ 不要在加入 Set 后修改元素!
one.setKey("3");

System.out.println("set.contains(one) = " + set.contains(one));
boolean addedAgain = set.add(one);
System.out.println("addedAgain = " + addedAgain);
System.out.println("set = " + set);
💥 输出示例:
java 复制代码
set = [1, 2]
set.contains(one) = false
addedAgain = true
set = [3, 2, 3]

📌 结论二:

修改对象后,该对象的 hashCode 改变,HashSet 无法识别出它之前已经存过,结果就把同一个对象当成了一个新的对象加入进来。

即使你在 Set 中 只是更新 key 的值,它也会当作是不同的元素对待,结果就是一个集合里"出现两个一样的对象"。


🔍 再验证一下:

java 复制代码
List<Key> list = new ArrayList<>(set);
Key key0 = list.get(0);
Key key2 = list.get(2);

System.out.println("key0 = " + key0);
System.out.println("key2 = " + key2);
System.out.println("key0 == key2 ? " + (key0 == key2));
💡 输出结果:
java 复制代码
key0 = 3
key2 = 3
key0 == key2 ? true

这说明:是同一个对象,但 HashSet 却认为它是不同的!


✅ 正确做法:使用不可变对象作为元素

java 复制代码
    final class SafeKey {
        private final String key;

        public SafeKey(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof SafeKey)) return false;
            SafeKey other = (SafeKey) o;
            return Objects.equals(this.key, other.key);
        }

        @Override
        public int hashCode() {
            return key.hashCode();
        }

        @Override
        public String toString() {
            return key;
        }
    }

再试试:

java 复制代码
Set<SafeKey> safeSet = new HashSet<>();
safeSet.add(new SafeKey("1"));
safeSet.add(new SafeKey("2"));
System.out.println("safeSet.contains(new SafeKey(\"1\")) = " + safeSet.contains(new SafeKey("1")));  // true

🎯 总结

规则 建议
HashSet 的底层是 HashMap 元素是作为 key 存储的
Key 必须是 hash 值稳定的 ❌ 不要用可变对象
修改对象后再添加 可能造成"集合污染"
添加后再修改 会导致 contains() 判断失败、重复添加
使用不可变对象 ✅ 最佳实践,推荐 StringInteger 等或自定义不可变类

你可以加一句口号强化记忆:

"对象一变,Set 就乱。"

相关推荐
徐小夕1 小时前
花了一周,3亿tokens,我开源了一款 Word 文档智能审查平台,文稿自动质检+可视化分析,告别低效人工审核
前端·算法·github
Ai拆代码的曹操2 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
kyriewen3 小时前
别再乱用useEffect了——你写的10个里有8个不该存在
前端·javascript·react.js
Ivanqhz3 小时前
Rust &‘static str浅析
java·前端·javascript·rust
IT_陈寒3 小时前
SpringBoot这个分页坑,我踩了三天才爬出来
前端·人工智能·后端
颜酱3 小时前
05 | 召回前置准备:根据业务数据库生成各数据库(读取配置阶段)
前端·人工智能·后端
Wang's Blog3 小时前
Go-Zero项目开发24: 基于Bitmap实现群聊消息已读未读
开发语言·后端·golang
zandy10114 小时前
衡石 Agentic BI的ReAct 推理框架在 Agentic BI 中的工程化实践
前端·javascript·react.js
罗超驿4 小时前
JavaEE进阶之路:从Web架构原理到HTML标签全解析
前端·html·web·javaee
西安小哥4 小时前
从前端到AI工程师:一场跨越鸿沟的真实蜕变之旅
前端