259. Java 集合 - Java 开发关键要点:选择不可变类型作为 Map 键的深度解析

21 阅读3分钟

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 候选

  • String
  • Integer, 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 的状态🚫 禁止操作会导致无法访问原有数据

“键一动,值成空。”

通过实验代码演示这个“丢失值”的现象,更能加深印象。