259. Java 集合 - Java 开发关键要点:选择不可变类型作为 Map 键的深度解析
🎯 为什么不能使用可变对象作为 Map 的 key?
在 Java 中,Map 的键(key)必须具有稳定的 equals() 和 hashCode() 值。一旦你把某个对象作为键放入 Map,它的行为就必须是“固定不变”的,否则,Map 的查找机制就会出问题,甚至导致你无法再找回对应的值。
使用可变对象作为键是一种严重的反面模式(antipattern),可能带来严重后果:使 map 中的键值对变得不可达,就像数据“丢失”了一样。
⚠️ 示例:可变键导致的严重问题
来看一个 错误示范(此代码仅作演示,实际开发请避免):
// ❌ Antipattern:可变 key 类
class Key {
private String key;
public Key(String key) {
this.key = key;
}
public String getKey() {
return 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 == null || getClass() != o.getClass()) return false;
Key other = (Key) o;
return Objects.equals(this.key, other.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}
我们使用这个类作为 HashMap 的键:
Key one = new Key("1");
Key two = new Key("2");
Map<Key, String> map = new HashMap<>();
map.put(one, "one");
map.put(two, "two");
System.out.println("map.get(one) = " + map.get(one)); // 输出 one
System.out.println("map.get(two) = " + map.get(two)); // 输出 two
目前一切正常。
🔧 然而……如果我们改变了 key 的值呢?
one.setKey("5"); // 修改了 key 的内部状态!
System.out.println("map.get(one) = " + map.get(one)); // ❌ null
System.out.println("map.get(new Key(1)) = " + map.get(new Key("1"))); // ❌ null
System.out.println("map.get(new Key(5)) = " + map.get(new Key("5"))); // ❌ null
🧨 输出结果:
map.get(one) = null
map.get(new Key(1)) = null
map.get(new Key(5)) = null
值“还在”Map里,但我们再也拿不到了,因为其对应的哈希桶已经找不到该 key 了。
🔁 再来一个:修改为已有的 key 值
one.setKey("2"); // 现在 one 和 two 都是 "2"
System.out.println("map.get(one) = " + map.get(one));
System.out.println("map.get(two) = " + map.get(two));
System.out.println("map.get(new Key(1)) = " + map.get(new Key("1")));
System.out.println("map.get(new Key(2)) = " + map.get(new Key("2")));
🧨 输出结果:
map.get(one) = two
map.get(two) = two
map.get(new Key(1)) = null
map.get(new Key(2)) = two
现在 one 被“劫持”到了 two 的值,而原始的 "1" 键值对彻底失联。😨
✅ 正确做法:使用不可变类型
在 Java 中,以下类型是常见的 不可变 key 候选:
StringInteger,Long,Double(所有包装类)- 自定义类(只要所有字段都为
final,且不提供修改器)
✅ 示例:不可变 key 类
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();
}
}
现在我们使用它作为 Map 的 key:
Map<SafeKey, String> map = new HashMap<>();
map.put(new SafeKey("1"), "one");
map.put(new SafeKey("2"), "two");
System.out.println(map.get(new SafeKey("1"))); // ✅ 输出 one
System.out.println(map.get(new SafeKey("2"))); // ✅ 输出 two
值永远不会“失联”,因为 key 是不可变的。
✅ 总结建议
| 做法 | 是否推荐 | 原因 |
|---|---|---|
使用 String 作为 key | ✅ 推荐 | 不可变,hashCode 稳定 |
| 使用自定义不可变类作为 key | ✅ 推荐 | 高可控性 |
| 使用自定义可变类作为 key | ❌ 强烈不推荐 | 存在值丢失、访问失败等风险 |
| 修改 key 的状态 | 🚫 禁止操作 | 会导致无法访问原有数据 |
“键一动,值成空。”
通过实验代码演示这个“丢失值”的现象,更能加深印象。