对于可变键(mutable key),根据您的预期行为,可能会出现两个非常不同的问题。
第一个问题:(可能是最琐碎的问题,但它让我遇到了一些我没有考虑过的问题!)
您正在尝试通过更新和修改相同的键对象将键值对放入映射中。您可能会执行类似以下代码的操作:Map<Integer, String>
并简单地说:
int key = 0;
loop {
map.put(key++, newString);
}
我正在重用 "对象" key
来创建一个映射表。在 Java 中,由于自动装箱的存在,每个新值的 key
都会自动装箱成一个新的 Integer 对象,这样就可以正常工作。但如果我创建了自己的(可变)Integer 对象,则不起作用:
MyInteger {
int value;
plusOne(){
value++;
}
}
接着尝试了相同的方法:
MyInteger key = new MyInteger(0);
loop{
map.put(key.plusOne(), newString)
}
我的期望是,例如,我将0映射为"a",将1映射为"b"。在第一个例子中,如果我更改int key = 0,则map将(正确地)给我"a"。为了简单起见,让我们假设MyInteger始终返回相同的hashCode()(如果你能以某种方式创建唯一的hashCode值,适用于对象的所有可能状态,那么这不是问题,你应该得到奖励)。在这种情况下,我调用0->"a",所以现在map保存了我的key并将其映射到"a",然后我修改key = 1并尝试放置1->"b"。
出现问题了! hashCode()相同,HashMap中唯一的键是我的MyInteger key对象,它刚刚被修改为等于1,因此它覆盖了该键的值,所以现在,代替0->"a"和1->"b"的map只剩下1->"b"
! 更糟糕的是,如果我改回key = 0,则hashCode指向1->"b",但由于HashMap的唯一键
是我的key对象,它满足相等性检查并返回"b",而不是预期的"a"。
如果像我一样陷入这种问题,它很难进行诊断。为什么?因为如果你有一个不错的hashCode()函数,它将生成(大多数)唯一值。哈希值在构造映射时很好地解决了不等式问题,但是如果你有足够的值,最终会在哈希值上发生碰撞,然后你将得到意外和主要无法解释的结果。 结果行为是它适用于小运行但对于大运行失败。
建议:
要查找此类问题,请修改hashCode()方法,甚至可以是微不足道的(即= 0 - 显然,在执行此操作时,请记住哈希值应该相同于两个相等对象*),并查看您是否获得相同的结果--因为您应该获得,如果您没有获得,则可能存在使用哈希表的实现的语义错误。
*如果总是从hashCode()返回0(尽管这会破坏哈希表的目的),则不应该有危险(如果有危险,那么您有语义问题)。 但这就是重点:hashCode是一种“快速简便”的相等性测量方法,而不是确切的方法。 因此,两个非常不同的对象可能具有相同的hashCode(),但却不相等。 另一方面,两个相等的对象必须始终具有相同的hashCode()值。
p.s. 据我了解,在Java中,如果您做出这样可怕的事情(例如有许多hashCode()冲突),它将开始使用红黑树而不是ArrayList。 所以当您期望O(1)查找时,您将得到O(log(n))-这比ArrayList更好,后者会给出O(n)。
第二个问题:
这似乎是大多数其他人关注的问题,所以我会尝试简要说明一下。 在此用例中,我尝试映射键值对,然后对键执行一些操作,然后想回来获取我的值。
期望:映射key -> value
,然后我修改key
并尝试get(key)
。 我期望会给我value
。
对我来说,这似乎很明显行不通,但我并不排除之前曾尝试过将集合作为键使用(并很快意识到它行不通)。 这并不起作用,因为key
的哈希值很可能已经改变,因此您甚至都不会在正确的桶中查找。
这就是为什么不建议使用集合作为键的原因。 我会假设,如果您这样做,正在尝试建立多对一关系。 因此,我有一个类(如教学),我希望两个小组进行两个不同的项目。 我想要的是,给定一个小组,他们的项目是什么? 简单,我把班级分成两部分,然后我有group1 -> project1
和group2 -> project2
。 但等等! 一个新学生到达,所以我把他们放在group1
中。 问题在于group1
现在已被修改,而且很可能其哈希值已更改,因此尝试执行get(group1)
可能会失败,因为它将在HashMap的错误或不存在的桶中查找。
上述问题的明显解决方案是链接事物-而不是使用小组作为键,给它们标签(不更改)指向小组,因此指向项目:g1 -> group1
和g1 -> project1
等。
p.s.
请确保为您希望用作键的任何对象定义
hashCode()
和
equals(...)
方法(Eclipse和我假设大多数IDE都可以为您完成此操作)。
代码示例:
这是一个展示两种不同“问题”行为的类。在这种情况下,我尝试将
0 ->“a”
,
1 ->“b”
和
2 ->“c”
映射到它们各自的对象中。在第一个问题中,我通过修改相同的对象来实现,而在第二个问题中,我使用唯一的对象,在第二个问题“已修复”的情况下,我克隆了这些唯一的对象。之后,我取其中一个“唯一”的键(
k0
)并修改它以尝试访问该映射。当键为
3
时,我期望会得到
a,b,c
和
null
。
然而,实际发生的是:
map.get(0) map1: 0 -> null, map2: 0 -> a, map3: 0 -> a
map.get(1) map1: 1 -> null, map2: 1 -> b, map3: 1 -> b
map.get(2) map1: 2 -> c, map2: 2 -> a, map3: 2 -> c
map.get(3) map1: 3 -> null, map2: 3 -> null, map3: 3 -> null
第一个映射(“第一个问题”)失败,因为它只包含一个键,该键最后更新并放置为等于
2
,因此当
k0 = 2
时,它正确地返回
"c"
,但对于其他两个键则返回
null
(单个键不等于0或1)。第二个映射有两个错误:最明显的是,当我要求
k0
时,它返回
"b"
(因为它已被修改-这是“第二个问题”,当你做这样的事情时似乎很明显)。它第二次失败是在修改
k0 = 2
后返回
"a"
(我希望它是
"c"
)。这更多是由于“第一个问题”:存在哈希码冲突,抢占者是相等检查-但是该映射保存了
k0
,它(显然是我-理论上可能与其他人不同)首先进行了检查,因此返回了第一个值,
"a"
,即使它继续检查,
"c"
也将匹配。最后,第三个映射完美地工作,因为我强制执行映射始终保持唯一键,无论我做什么(在插入期间克隆对象)。
我想澄清,我同意,
克隆不是解决方案!我只是举了一个例子,说明为什么映射需要唯一键以及如何强制执行唯一键“修复”问题。
public class HashMapProblems {
private int value = 0;
public HashMapProblems() {
this(0);
}
public HashMapProblems(final int value) {
super();
this.value = value;
}
public void setValue(final int i) {
this.value = i;
}
@Override
public int hashCode() {
return value % 2;
}
@Override
public boolean equals(final Object o) {
return o instanceof HashMapProblems
&& value == ((HashMapProblems) o).value;
}
@Override
public Object clone() {
return new HashMapProblems(value);
}
public void reset() {
this.value = 0;
}
public static void main(String[] args) {
final HashMapProblems k0 = new HashMapProblems(0);
final HashMapProblems k1 = new HashMapProblems(1);
final HashMapProblems k2 = new HashMapProblems(2);
final HashMapProblems k = new HashMapProblems();
final HashMap<HashMapProblems, String> map1 = firstProblem(k);
final HashMap<HashMapProblems, String> map2 = secondProblem(k0, k1, k2);
final HashMap<HashMapProblems, String> map3 = secondProblemFixed(k0, k1, k2);
for (int i = 0; i < 4; ++i) {
k0.setValue(i);
System.out.printf(
"map.get(%d) map1: %d -> %s, map2: %d -> %s, map3: %d -> %s",
i, i, map1.get(k0), i, map2.get(k0), i, map3.get(k0));
System.out.println();
}
}
private static HashMap<HashMapProblems, String> firstProblem(
final HashMapProblems start) {
start.reset();
final HashMap<HashMapProblems, String> map = new HashMap<>();
map.put(start, "a");
start.setValue(1);
map.put(start, "b");
start.setValue(2);
map.put(start, "c");
return map;
}
private static HashMap<HashMapProblems, String> secondProblem(
final HashMapProblems... keys) {
final HashMap<HashMapProblems, String> map = new HashMap<>();
IntStream.range(0, keys.length).forEach(
index -> map.put(keys[index], "" + (char) ('a' + index)));
return map;
}
private static HashMap<HashMapProblems, String> secondProblemFixed(
final HashMapProblems... keys) {
final HashMap<HashMapProblems, String> map = new HashMap<>();
IntStream.range(0, keys.length)
.forEach(index -> map.put((HashMapProblems) keys[index].clone(),
"" + (char) ('a' + index)));
return map;
}
}
一些注意事项:
需要注意的是,上面的例子中map1
只保存了两个值,这是由于我设置了hashCode()
函数来区分奇数和偶数。因此,k = 0
和k = 2
具有相同的hashCode
值0
。因此,当我修改k = 2
并尝试将k -> "c"
映射到k -> "a"
时,映射k -> "b"
仍然存在,因为它存在于另一个桶中。
此外,在上面的代码中,有许多不同的方法可以检查映射表,我鼓励那些好奇心强的人做一些事情,例如打印出映射表的值,然后打印键值映射(你可能会惊讶于得到的结果)。尝试更改不同的“唯一”键(即k0
、k1
和k2
),尝试更改单个键k
。您还可以看看即使secondProblemFixed
已经被“修复”,实际上它也没有真正解决,因为您还可以通过Map::keySet
获得访问键,并对它们进行修改。