【发布时间】:2015-12-03 08:23:33
【问题描述】:
为这个相当幼稚的问题道歉,但我相信我自己的回答是幼稚的。我认为键(在 HashTables 中)是不可变的,因为我们不想以某种方式意外更改键,从而弄乱 HashTable 的排序。这是一个正确的解释吗?如果是这样,如何更正确?
【问题讨论】:
为这个相当幼稚的问题道歉,但我相信我自己的回答是幼稚的。我认为键(在 HashTables 中)是不可变的,因为我们不想以某种方式意外更改键,从而弄乱 HashTable 的排序。这是一个正确的解释吗?如果是这样,如何更正确?
【问题讨论】:
在HashTable.put 期间,键被散列并且它的值被存储在基于散列的多个存储桶(它们是键值对的列表)之一中,例如:
bucket[key.hashcode() % numberOfBuckets].add(key, value)
如果密钥的 hashcode 在插入后发生更改,则它可能位于错误的存储桶中,然后您将无法找到它,并且哈希表将在该密钥的任何 get 上错误地返回 null。
旁白:了解哈希表的内部工作原理有助于您了解优质hashcode 函数对您的键的重要性。因为糟糕的哈希码函数可能会导致桶中键的分布不均。由于桶只是列表,这会导致大量线性搜索,从而大大降低哈希表的有效性。例如这个可怕的哈希码函数将所有内容放在一个桶中,因此它实际上只是一个列表。
public int hashcode { return 42; /*terrible hashcode example, don't use!*/ }
这也是质数出现在好的哈希码函数中的原因之一,例如:
public int hashcode {
int hash = field1.hashcode();
hash = hash*31 + field2.hashcode(); //note the prime 31
hash = hash*31 + field3.hashcode();
return hash;
}
【讨论】:
总体思路是正确的,但细节不正确。
HashTable 中的键不必是不可变的,它是调用 hashCode()(和 equals)方法的结果,需要保持不可变和一致 (也就是说,哈希表的行为是可预测的)。
从高级的角度来看,这是因为哈希表的工作方式:当插入 (key, value) 对时,key 的 hashCode 在内部用于计算出一个“桶”将放置值。而当value 被key 检索到时,hashCode 被再次计算,以找到桶。
现在,如果在插入和检索之间的任何时间点,调用 hashCode 的结果发生变化,“查找桶”将与“插入”桶不同,事情将无法预测。
总而言之,给定一个看起来像这样的 Key 对象(两个内部字符串组成对象,但在 hashCode / equals 中只考虑了一个 partOfHashCode):
public static class Key {
private String partOfHashCode;
private String notPartOfHashCode;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((partOfHashCode == null) ? 0 : partOfHashCode.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Key other = (Key) obj;
if (partOfHashCode == null) {
if (other.partOfHashCode != null)
return false;
} else if (!partOfHashCode.equals(other.partOfHashCode))
return false;
return true;
}
}
这样用就好了:
public static void main(String[] args) {
Map<Key, String> myMap = new HashMap<>();
Key key = new Key();
key.partOfHashCode = "myHash";
myMap.put(key, "value");
key.notPartOfHashCode = "mutation of the key, but not of its hash/equals definition";
System.out.println(myMap.get(key));
}
(这会在控制台中记录“值”对象)。
但是这样用就不好了
public static void main(String[] args) {
Map<Key, String> myMap = new HashMap<>();
Key key = new Key();
key.partOfHashCode = "myHash";
myMap.put(key, "value");
key.partOfHashCode = "mutation of the hashCode of the key";
System.out.println(myMap.get(key));
}
(最后一个示例可以在控制台中记录“null”)。
有关此主题的更多信息,您还应该阅读 hashCode / equals 一致性。
【讨论】:
Java 没有内在的保证HashTable-keys 是不可变的。甚至不能保证他们的hashcode 保持不变。但是,如果您添加具有可变 hashCode 的键,您就有麻烦了。假设您要插入一个 hashCode 为 1 的键。然后将其插入对应于 1 的哈希桶中。然后将对象更改为具有 2 的 hashCode 并调用 hashMap.get(key)。当对象仍在hashTable 中时,系统会在对应于 2 的存储桶中查找,但不会在那里找到它。您甚至无法remove 该条目,因为它不会被发现。
tl;dr 为了使您的应用程序正常工作HashTable-keys 需要具有不可变的hashcodes,但您必须自己处理这个事实。
【讨论】: