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() 判断失败、重复添加 |
| 使用不可变对象 | ✅ 最佳实践,推荐 String、Integer 等或自定义不可变类 |
你可以加一句口号强化记忆:
"对象一变,Set 就乱。"