我们如何确定最佳hashCode()
方法的集合实现(假设equals方法已被正确覆盖)?
collection.hashCode()
(hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/...)
我们如何确定最佳hashCode()
方法的集合实现(假设equals方法已被正确覆盖)?
collection.hashCode()
(hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/...)
Answers:
最好的实现?这是一个难题,因为它取决于使用模式。
在几乎所有情况下,Josh Bloch的 有效Java项目8(第二版)中都提出了合理的良好实现。最好的办法是在那里查找,因为作者在那里解释了为什么这种方法很好。
创建一个int result
并分配一个非零值。
对于在方法中测试的每个字段 f
,通过以下equals()
方式计算哈希码c
:
boolean
:计算(f ? 0 : 1)
;byte
,char
,short
或int
:计算(int)f
;long
:计算(int)(f ^ (f >>> 32))
;float
:计算Float.floatToIntBits(f)
;double
:计算Double.doubleToLongBits(f)
和处理返回值,就像每个长值一样;hashCode()
方法的结果或0 f == null
;将哈希值c
与result
:
result = 37 * result + c
返回 result
对于大多数使用情况,这应该导致哈希值的正确分配。
如果您对dmeister推荐的有效Java实现感到满意,则可以使用库调用而不是自己滚动:
@Override
public int hashCode() {
return Objects.hashCode(this.firstName, this.lastName);
}
这需要Guava(com.google.common.base.Objects.hashCode
)或Java 7(java.util.Objects.hash
)中的标准库,但工作方式相同。
hashCode
是,如果您有一个custom equals
,这正是这些库方法设计的目的。文档非常明确了它们与equals
。库实现并不能声称免除您知道正确hashCode
实现的特征是什么-这些库使您可以在大多数被覆盖的情况下更轻松地实现这种一致的实现equals
。
java.util.Objects.hash(...)
方法而不是guava com.google.common.base.Objects.hashCode(...)
方法。我认为大多数人会选择标准库而不是额外的依赖项。
hashCode()
对于数组来说,它只是它的java.lang.System.identityHashCode(...)
。
最好使用Eclipse提供的功能,该功能做得很好,您可以投入精力和精力来开发业务逻辑。
尽管这与Android
文档(Wayback Machine)和Github上的我自己的代码链接在一起,但它通常适用于Java。我的答案是dmeister的Answers的扩展,仅提供了易于阅读和理解的代码。
@Override
public int hashCode() {
// Start with a non-zero constant. Prime is preferred
int result = 17;
// Include a hash for each field.
// Primatives
result = 31 * result + (booleanField ? 1 : 0); // 1 bit » 32-bit
result = 31 * result + byteField; // 8 bits » 32-bit
result = 31 * result + charField; // 16 bits » 32-bit
result = 31 * result + shortField; // 16 bits » 32-bit
result = 31 * result + intField; // 32 bits » 32-bit
result = 31 * result + (int)(longField ^ (longField >>> 32)); // 64 bits » 32-bit
result = 31 * result + Float.floatToIntBits(floatField); // 32 bits » 32-bit
long doubleFieldBits = Double.doubleToLongBits(doubleField); // 64 bits (double) » 64-bit (long) » 32-bit (int)
result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));
// Objects
result = 31 * result + Arrays.hashCode(arrayField); // var bits » 32-bit
result = 31 * result + referenceField.hashCode(); // var bits » 32-bit (non-nullable)
result = 31 * result + // var bits » 32-bit (nullable)
(nullableReferenceField == null
? 0
: nullableReferenceField.hashCode());
return result;
}
编辑
通常,当您覆盖时hashcode(...)
,您还希望覆盖equals(...)
。因此,对于那些将要实施或已经实施的人equals
,这里是我的Github的很好的参考...
@Override
public boolean equals(Object o) {
// Optimization (not required).
if (this == o) {
return true;
}
// Return false if the other object has the wrong type, interface, or is null.
if (!(o instanceof MyType)) {
return false;
}
MyType lhs = (MyType) o; // lhs means "left hand side"
// Primitive fields
return booleanField == lhs.booleanField
&& byteField == lhs.byteField
&& charField == lhs.charField
&& shortField == lhs.shortField
&& intField == lhs.intField
&& longField == lhs.longField
&& floatField == lhs.floatField
&& doubleField == lhs.doubleField
// Arrays
&& Arrays.equals(arrayField, lhs.arrayField)
// Objects
&& referenceField.equals(lhs.referenceField)
&& (nullableReferenceField == null
? lhs.nullableReferenceField == null
: nullableReferenceField.equals(lhs.nullableReferenceField));
}
首先,确保equals正确实现。从IBM DeveloperWorks文章中:
- 对称性:对于两个引用a和b,当且仅当b.equals(a)时,a.equals(b)
- 自反性:对于所有非空引用,a.equals(a)
- 传递性:如果a.equals(b)和b.equals(c),则a.equals(c)
然后确保它们与hashCode的关系尊重该联系人(来自同一篇文章):
- 与hashCode()的一致性:两个相等的对象必须具有相同的hashCode()值
最后,一个好的哈希函数应该努力接近理想的哈希函数。
about8.blogspot.com,您说过
如果equals()对两个对象返回true,则hashCode()应该返回相同的值。如果equals()返回false,则hashCode()应该返回不同的值
我不同意你的看法。如果两个对象具有相同的哈希码,则不必表示它们相等。
如果A等于B,则A.hashcode必须等于B.hascode
但
如果A.hashcode等于B.hascode,则并不意味着A必须等于B
(A != B) and (A.hashcode() == B.hashcode())
,这就是我们所说的哈希函数碰撞。这是因为哈希函数的共域始终是有限的,而其域通常不是。共域越大,冲突发生的频率就越小。良好的散列函数应针对给定的特定共域大小,针对不同的对象返回不同的散列,并且最大可能实现。但是,它很少能得到完全保证。
如果使用eclipse,则可以生成equals()
并hashCode()
使用:
源->生成hashCode()和equals()。
使用此函数,您可以确定要用于相等性和哈希码计算的字段,然后Eclipse生成相应的方法。
有一个很好的贯彻实施有效的Java的hashcode()
和equals()
逻辑的Apache Commons Lang中。检出HashCodeBuilder和EqualsBuilder。
Objects
类从Java7开始提供hash(Object ..args)
&equals()
方法。对于使用jdk 1.7+的任何应用程序,建议使用这些工具
IdentityHashMap
)。FWIW我使用基于id的hashCode并等于所有实体。
简要说明一下完成其他更详细的答案(根据代码):
如果我考虑如何在Java中创建哈希表(尤其是jGuru FAQ条目)的问题,我认为可以判断哈希码的其他一些标准是:
如果我正确理解了您的问题,则您有一个自定义集合类(即从Collection接口扩展的新类),并且您想实现hashCode()方法。
如果您的集合类扩展了AbstractList,那么您不必担心它,已经存在equals()和hashCode()的实现,该实现通过迭代所有对象并将其hashCodes()加在一起而起作用。
public int hashCode() {
int hashCode = 1;
Iterator i = iterator();
while (i.hasNext()) {
Object obj = i.next();
hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
}
return hashCode;
}
现在,如果您想要的是计算特定类的哈希码的最佳方法,那么我通常使用^(按位互斥或)运算符来处理equals方法中使用的所有字段:
public int hashCode(){
return intMember ^ (stringField != null ? stringField.hashCode() : 0);
}
@ about8:那里有一个非常严重的错误。
Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");
相同的哈希码
你可能想要像
public int hashCode() {
return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();
(这些天您可以直接从Java中的int获取hashCode吗?我认为它会进行一些自动广播。.如果是这种情况,请跳过toString,这很丑陋。)
foo
并bar
导致相同hashCode
。您的toString
AFAIK无法编译,如果可以编译,那么效率很低。诸如此类的东西109 * getFoo().hashCode() + 57 * getBar().hashCode()
更快,更简单,并且不会产生不必要的冲突。
正如您特别要求的集合一样,我想添加一个其他答案尚未提及的方面:HashMap不会期望将其键添加到集合后更改其哈希码。会打败整个目标...
在Apache Commons EqualsBuilder和HashCodeBuilder上使用反射方法。
我使用了一个小型包装器,Arrays.deepHashCode(...)
因为它可以正确处理作为参数提供的数组
public static int hash(final Object... objects) {
return Arrays.deepHashCode(objects);
}
任何将哈希值均匀分布在可能范围内的哈希方法都是不错的实现。看到有效的java(http://books.google.com.au/books?id=ZZOiqZQIbRMC&dq=effective+java&pg=PP1&ots=UZMZ2siN25&sig=kR0n73DHJOn-D77qGj0wOxAxiZw&hl=en&sa=X&oi=book_result&resnum=1&ct=result),有一个很好的提示在那里实现哈希码(我认为是项目9)。
这是另一个介绍了超类逻辑的JDK 1.7+方法演示。我认为,使用Object类的hashCode(),纯JDK依赖关系和无需任何额外的手工工作,这非常方便。请注意,它Objects.hash()
是null容忍的。
我没有任何equals()
实现,但是实际上您当然会需要它。
import java.util.Objects;
public class Demo {
public static class A {
private final String param1;
public A(final String param1) {
this.param1 = param1;
}
@Override
public int hashCode() {
return Objects.hash(
super.hashCode(),
this.param1);
}
}
public static class B extends A {
private final String param2;
private final String param3;
public B(
final String param1,
final String param2,
final String param3) {
super(param1);
this.param2 = param2;
this.param3 = param3;
}
@Override
public final int hashCode() {
return Objects.hash(
super.hashCode(),
this.param2,
this.param3);
}
}
public static void main(String [] args) {
A a = new A("A");
B b = new B("A", "B", "C");
System.out.println("A: " + a.hashCode());
System.out.println("B: " + b.hashCode());
}
}
标准实现很弱,使用它会导致不必要的冲突。想象一个
class ListPair {
List<Integer> first;
List<Integer> second;
ListPair(List<Integer> first, List<Integer> second) {
this.first = first;
this.second = second;
}
public int hashCode() {
return Objects.hashCode(first, second);
}
...
}
现在,
new ListPair(List.of(a), List.of(b, c))
和
new ListPair(List.of(b), List.of(a, c))
具有相同的hashCode
,即31*(a+b) + c
用于List.hashCode
此处的乘法器在这里被重用。显然,冲突是不可避免的,但产生不必要的冲突只是...不必要。
使用基本上没有什么聪明的31
。为了避免丢失信息,乘法器必须为奇数(任何偶数乘法器至少会丢失最高有效位,四个的倍数会丢失两个,依此类推)。任何奇数乘数都是可用的。较小的乘法器可能会导致更快的计算速度(JIT可以使用移位和加法运算),但是鉴于乘法在现代Intel / AMD上的延迟只有三个周期,因此这无关紧要。较小的乘数也会导致较小输入的更多冲突,有时这可能是个问题。
使用质数是没有意义的,因为质数在环Z /(2 ** 32)中没有意义。
因此,我建议使用随机选择的大奇数(随意取质数)。由于i86 / amd64 CPU可以将较短的指令用于将操作数装入单个带符号的字节,因此乘法器(如109)在速度上有很小的优势。要最大程度地减少冲突,请采用诸如0x58a54cf5之类的方法。
在不同的地方使用不同的乘法器会有所帮助,但可能不足以证明其他工作的合理性。
合并哈希值时,通常使用boost c ++库中使用的合并方法,即:
seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
这在确保均匀分配方面做得相当不错。有关此公式的工作原理的一些讨论,请参见StackOverflow帖子:boost :: hash_combine中的幻数
在以下位置对不同的哈希函数进行了很好的讨论:http : //burtleburtle.net/bob/hash/doobs.html
对于一个简单的类,通常最容易基于由equals()实现检查的类字段来实现hashCode()。
public class Zam {
private String foo;
private String bar;
private String somethingElse;
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Zam otherObj = (Zam)obj;
if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) {
if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) {
return true;
}
}
return false;
}
public int hashCode() {
return (getFoo() + getBar()).hashCode();
}
public String getFoo() {
return foo;
}
public String getBar() {
return bar;
}
}
最重要的是使hashCode()和equals()保持一致:如果equals()对两个对象返回true,则hashCode()应该返回相同的值。如果equals()返回false,则hashCode()应该返回不同的值。
("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc")
。这是严重的缺陷。最好评估两个字段的哈希码,然后计算它们的线性组合(最好使用素数作为系数)。
foo
以及bar
产生不必要的碰撞,太。
Objects.hashCode(collection)
应该是一个完美的解决方案!