1. 概述
在 Java 中,*HashMap* 是一种广泛使用的数据结构,它以键值对形式存储元素,提供快速的数据访问和检索功能。使用 HashMap 时,我们有时需要修改已有条目的键。
本教程将探讨如何在 Java 中修改 HashMap 的键。
2. 使用 remove() 后 put() 的方法
首先了解 HashMap 如何存储键值对。HashMap 内部使用 Node 类型维护键值对:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
...
}
可见,key 声明使用了 final 关键字。因此,我们无法在将键对象放入 HashMap 后重新赋值。
虽然不能直接替换键,但可以通过其他方式实现目标。换个角度思考问题:
假设 HashMap 中有条目 K1 -> V。现在想将 K1 改为 K2,得到 K2 -> V。最直接的方法是找到 K1 条目并替换键,但*更可靠的做法是移除 K1 -> V 关联,然后添加新条目 K2 -> V。*
Map 接口提供了 remove(key) 方法通过键移除条目。重要的是,remove() 方法会返回被移除的值。
通过示例说明这种方案。为简化演示,使用单元测试断言验证结果:
Map<String, Integer> playerMap = new HashMap<>();
playerMap.put("Kai", 42);
playerMap.put("Amanda", 88);
playerMap.put("Tom", 200);
这段代码创建了一个 HashMap,存储玩家名称(String)和分数(Integer)的映射。现在将条目 "Kai" -> 42 中的玩家名改为 *"Eric"*:
// 将 Kai 替换为 Eric
playerMap.put("Eric", playerMap.remove("Kai"));
assertFalse(playerMap.containsKey("Kai"));
assertTrue(playerMap.containsKey("Eric"));
assertEquals(42, playerMap.get("Eric"));
单行语句 playerMap.put("Eric", playerMap.remove("Kai"));
完成了两个操作:移除键为 "Kai" 的条目(获取其值 42),然后添加新条目 "Eric" -> 42。
测试通过,证明方案有效。但可能引发疑问:HashMap 的键是 final 变量,不能重新赋值。但我们可以修改 final 对象的值。在 playerMap 示例中键是 String,由于 *String 不可变性 无法修改值。那么如果键是可变对象,能否通过修改键值解决问题?
接下来深入分析。
3. 永远不要修改 HashMap 中的键
在 Java 中应避免使用可变对象作为 HashMap 的键,这会导致潜在问题和意外行为。
因为 HashMap 的键对象用于计算哈希码,该值决定了存储对应值的桶位置。 如果键是可变的且在作为键后被修改,其哈希码也会改变。结果将无法正确检索关联值,因为值会被定位到错误的桶中。
通过示例理解这点。首先创建只有一个属性的 Player 类:
class Player {
private String name;
public Player(String name) {
this.name = name;
}
// getter 和 setter 方法省略
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Player)) {
return false;
}
Player player = (Player) o;
return name.equals(player.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
Player 类提供了 name 属性的 setter,因此是可变的。*hashCode()* 方法使用 name 属性计算哈希码,修改 Player 对象的 name 会改变其哈希码。
接下来创建映射,使用 Player 对象作为键:
Map<Player, Integer> myMap = new HashMap<>();
Player kai = new Player("Kai");
Player tom = new Player("Tom");
Player amanda = new Player("Amanda");
myMap.put(kai, 42);
myMap.put(amanda, 88);
myMap.put(tom, 200);
assertTrue(myMap.containsKey(kai));
现在将玩家 kai 的 name 从 "Kai" 改为 *"Eric"*,验证结果:
// 将 Kai 的名字改为 Eric
kai.setName("Eric");
assertEquals("Eric", kai.getName());
Player eric = new Player("Eric");
assertEquals(eric, kai);
// 此时映射中既找不到 Kai 也找不到 Eric:
assertFalse(myMap.containsKey(kai));
assertFalse(myMap.containsKey(eric));
*修改 kai 的 name 为 "Eric" 后,无法通过 kai 或 eric 检索到条目 "Eric" -> 42。* 但 Player("Eric") 对象确实存在于映射键中:
// 尽管 Player("Eric") 存在:
long ericCount = myMap.keySet()
.stream()
.filter(player -> player.getName()
.equals("Eric"))
.count();
assertEquals(1, ericCount);
理解原因需先了解 HashMap 工作原理:
HashMap 维护内部哈希表存储键的哈希码。哈希码引用映射条目。使用 get(key) 等方法检索条目时,HashMap 计算给定键对象的哈希码并在哈希表中查找。
在示例中,放入 kai("Kai") 时,基于字符串 "Kai" 计算哈希码(假设为 "hash-kai")并存入哈希表。修改 kai("Kai") 为 kai("Eric") 后,尝试通过 kai("Eric") 检索时,HashMap 计算得到 *"hash-eric"*,在哈希表中找不到对应项。
在实际应用中,这种意外行为的根本原因可能极难排查。
因此,应避免使用可变对象作为 HashMap 的键,且永远不要修改键对象。
4. 总结
本文介绍了通过 "remove() 后 put()" 方案替换 HashMap 键的方法。通过示例分析了为何应避免使用可变对象作为键,以及为何绝不能修改 HashMap 中的键。
完整示例代码可在 GitHub 获取。