我知道String类的hashCode()方法不能保证为不同的String-s生成唯一的哈希码。我看到了很多将String键放入HashMap-s的用法(使用默认的String hashCode()方法)。如果put
地图使用真正不同的String键替换了先前放置在地图上的HashMap条目,那么很多这种用法可能会导致重大的应用程序问题。
在String.hashCode()对于不同的String-s返回相同值的情况下,您遇到的几率是多少?当键是字符串时,开发人员如何解决此问题?
我知道String类的hashCode()方法不能保证为不同的String-s生成唯一的哈希码。我看到了很多将String键放入HashMap-s的用法(使用默认的String hashCode()方法)。如果put
地图使用真正不同的String键替换了先前放置在地图上的HashMap条目,那么很多这种用法可能会导致重大的应用程序问题。
在String.hashCode()对于不同的String-s返回相同值的情况下,您遇到的几率是多少?当键是字符串时,开发人员如何解决此问题?
Answers:
开发人员不必为了解决程序的正确性而在HashMap中解决哈希冲突的问题。
这里有一些关键的事情要理解:
如果需要,可以提供更多详细信息:
哈希的工作方式(尤其是在像Java的HashMap这样的哈希集合的情况下,这就是您所要求的):
HashMap将您提供给它的值存储在子集(称为存储桶)中。这些实际上是作为链接列表实现的。其中的数量有限:iirc,默认情况下为16,并且随着您在地图上放置更多项目而增加。存储桶总应该比值多。举一个例子,使用默认值,如果您向HashMap添加100个条目,将有256个存储桶。
可以在映射中用作键的每个值都必须能够生成一个称为哈希码的整数值。
HashMap使用此哈希码选择存储桶。最终,这意味着将整数值modulo
作为存储桶的数量,但是在此之前,Java的HashMap具有内部方法(称为hash()
),该方法调整哈希码以减少某些已知的聚集源。
查找值时,HashMap选择存储区,然后使用线性搜索链表,以搜索单个元素.equals()
。
所以:您不必为正确而解决冲突,通常也不必担心它们的性能,如果您使用的是本机Java类(例如String),则不必担心要么生成哈希码值。
如果您必须编写自己的哈希码方法(这意味着您已经编写了一个具有复合值的类,例如名字/姓氏对),则事情会变得稍微复杂一些。在这里很可能会出错,但这不是火箭科学。首先,要知道这一点:为了确保正确性,您必须要做的就是确保相等的对象产生相等的哈希码。因此,如果您为类编写一个hashcode()方法,则还必须编写一个equals()方法,并且必须检查每个方法中的相同值。
可以编写一个不好但正确的hashcode()方法,这意味着它可以满足“相等的对象必须产生相等的哈希码”约束,但是由于发生很多冲突,其性能仍然很差。
规范的退化最坏情况将是编写一种在所有情况下仅返回恒定值(例如3)的方法。这意味着每个值都将散列到同一存储桶中。
它仍然可以工作,但是性能会下降到链表的性能。
显然,您不会编写如此糟糕的hashcode()方法。如果您使用的是一个不错的IDE,它可以为您生成一个。由于StackOverflow喜欢代码,因此以下是上述firstname / lastname类的代码。
public class SimpleName {
private String firstName;
private String lastName;
public SimpleName(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result
+ ((lastName == null) ? 0 : lastName.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;
SimpleName other = (SimpleName) obj;
if (firstName == null) {
if (other.firstName != null)
return false;
} else if (!firstName.equals(other.firstName))
return false;
if (lastName == null) {
if (other.lastName != null)
return false;
} else if (!lastName.equals(other.lastName))
return false;
return true;
}
}
我强烈怀疑该HashMap.put
方法不能仅通过查看来确定密钥是否相同String.hashCode
。
绝对有可能发生哈希冲突,因此,如果确实存在两个s从返回的值相同的情况,则可以期望该String.equals
方法也将被调用以确保String
s真正相等String
。hashCode
。
因此,String
仅当且仅当by所返回的值相等且该方法返回时,才将新键判断为与String
已经存在于该键中的键相同。HashMap
hashCode
equals
true
另外要补充的是,这种思想对于之外的其他类也适用String
,因为Object
该类本身已经具有hashCode
和equals
方法。
编辑
因此,要回答这个问题,不,使用aString
作为a的键不是一个坏主意HashMap
。
这不是问题,而只是哈希表的工作方式。对于所有不同的字符串,都不可能有不同的哈希码,因为与整数相比,不同的字符串要多得多。
正如其他人所写的那样,哈希冲突是通过equals()方法解决的。这可能导致的唯一问题是哈希表的退化,从而导致性能下降。这就是Java的HashMap具有负载因子(存储桶与插入的元素之间的比率)的原因,如果超出该负载因子,则将导致存储桶数量翻倍的表重新哈希。
这通常效果很好,但前提是散列函数良好,即,对于特定输入集,其产生的碰撞次数不会超过统计上的预期数目。String.hashCode()
在这方面是很好的,但这并非总是如此。据称,在Java 1.2之前,它仅包含第n个字符。这样做速度更快,但是会导致共享第n个字符的所有String发生可预见的冲突-如果您运气不佳,无法进行此类常规输入,或者如果有人想对您的应用程序进行DOS攻击,那就非常糟糕。
我把你引到这里的答案。虽然使用字符串不是一个坏主意(@CPerkins完美解释了为什么),但将值存储在带有整数键的哈希图中更好,因为它通常更快(尽管并不明显)并且机会较低(实际上没有机会)的碰撞。
看到这个图表使用216553个键在各种情况下的碰撞,(从这个被盗后,重新格式化为我们的讨论)
Hash Lowercase Random UUID Numbers
============= ============= =========== ==============
Murmur 145 ns 259 ns 92 ns
6 collis 5 collis 0 collis
FNV-1a 152 ns 504 ns 86 ns
4 collis 4 collis 0 collis
FNV-1 184 ns 730 ns 92 ns
1 collis 5 collis 0 collis*
DBJ2a 158 ns 443 ns 91 ns
5 collis 6 collis 0 collis***
DJB2 156 ns 437 ns 93 ns
7 collis 6 collis 0 collis***
SDBM 148 ns 484 ns 90 ns
4 collis 6 collis 0 collis**
CRC32 250 ns 946 ns 130 ns
2 collis 0 collis 0 collis
Avg Time per key 0.8ps 2.5ps 0.44ps
Collisions (%) 0.002% 0.002% 0%
当然,整数的数量限制为2 ^ 32,因为对字符串的数量没有限制(并且对可以存储在中的键的数量也没有理论上的限制HashMap
)。如果使用long
(或什至float
),则冲突是不可避免的,因此没有比字符串更好的了。但是,即使发生哈希冲突,put()
也get()
将始终放置/获取正确的键值对(请参见下面的编辑)。
最后,它实际上并不重要,因此请使用更方便的方法。但是,如果方便没有影响,并且您不打算拥有超过2 ^ 32个条目,那么建议您将其ints
用作键。
编辑
尽管以上绝对正确,但String
出于性能原因,切勿使用“ StringKey” .hashCode()来代替原始密钥来生成密钥-2个不同的字符串可能具有相同的hashCode,从而导致put()
方法被覆盖。Java的实现HashMap
足够聪明,可以自动使用相同的哈希码处理字符串(实际上是任何类型的键),因此让Java为您处理这些事情是明智的。
您正在谈论哈希冲突。哈希冲突是一个问题,而与hashCode的类型无关。所有使用hashCode的类(例如HashMap)都可以很好地处理哈希冲突。例如,HashMap可以在每个存储桶中存储多个对象。
除非您自己调用hashCode,否则不必担心。哈希冲突虽然很少见,但不会破坏任何东西。