尽管Java语言不提供对关联数组的直接支持-可以将任何对象作为索引的数组-根Object
类中存在hashCode()
方法显然可以预见到HashMap
(及其前身, Hashtable
)。 在理想条件下,基于散列的容器可提供有效的插入和有效的检索。 直接在对象模型中支持哈希可以促进基于哈希的容器的开发和使用。
定义平等
Object
类有两种用于推断对象身份的方法: equals()
和hashCode()
。 通常,如果您覆盖这些方法之一,则必须覆盖这两种方法,因为它们之间必须维护重要的关系。 特别是,如果两个对象根据equals()
方法equals()
,则它们必须具有相同的hashCode()
值(尽管通常情况并非如此)。
给定类的equals()
语义留给实现者; 定义equals()
对给定类的含义是该类设计工作的一部分。 Object
提供的默认实现只是引用相等:
public boolean equals(Object obj) {
return (this == obj);
}
在此默认实现下,两个引用仅在引用完全相同的对象时才相等。 同样, Object
提供的hashCode()
的默认实现是通过将Object
的内存地址映射到整数值而派生的。 因为在某些体系结构上,地址空间大于int
的值范围,所以两个不同的对象可能具有相同的hashCode()
。 如果重写hashCode()
,则仍然可以使用System.identityHashCode()
方法访问此默认值。
覆盖equals()-一个简单的例子
equals()
和hashCode()
的基于身份的实现是明智的默认设置,但是对于某些类,希望稍微放松对相等的定义。 例如, Integer
类与此类似地定义equals()
:
public boolean equals(Object obj) {
return (obj instanceof Integer
&& intValue() == ((Integer) obj).intValue());
}
根据此定义, 只有两个Integer
对象包含相同的整数值,它们才相等。 加上Integer
是不可变的,这使得将Integer
用作HashMap
的键非常实用。 Java类库中的所有原始包装器类(例如Integer
, Float
, Character
和Boolean
)以及String
都使用这种基于值的相等性方法(如果两个String
对象包含相同的字符序列,则它们相等) )。 因为这些类是不可变的,并且明智地实现了hashCode()
和equals()
,所以它们都具有良好的哈希键。
为什么要覆盖equals()和hashCode()?
如果Integer
不重写equals()
和hashCode()
会发生什么? 没什么,如果我们从未在HashMap
或其他基于哈希的集合中使用Integer
作为键。 然而,如果我们使用这样的Integer
对象,该对象的关键HashMap
,我们将不能够可靠地检索相关的值,除非我们使用了完全相同的Integer
的情况下get()
调用正如我们在做put()
致电。 这将需要确保我们在整个程序中仅使用与特定整数值相对应的Integer
对象的单个实例。 不用说,这种方法会带来不便并且容易出错。
Object
的接口协定要求,如果两个对象根据equals()
,则它们必须具有相同的hashCode()
值。 当我们的根对象类的判别能力完全由equals()
所赋予时,为什么我们的根对象类需要hashCode()
equals()
? hashCode()
方法的存在纯粹是为了提高效率。 Java平台架构师预料到在典型的Java应用程序中,基于哈希的收集类(例如Hashtable
, HashMap
和HashSet
的重要性,并且使用equals()
与许多对象进行比较在计算上会非常昂贵。 具有每个Java对象支持的hashCode()
允许使用基于哈希的集合进行有效的存储和检索。
实现equals()和hashCode()的要求
对equals()
和hashCode()
的行为有一些限制,这些限制在Object
的文档中进行了枚举。 特别是, equals()
方法必须具有以下属性:
- 对称性:对于两个引用
a
和b
,当且仅当b.equals(a)
,a.equals(b)
b.equals(a)
- 自反性:对于所有非空引用,
a.equals(a)
- 传递性:如果
a.equals(b)
和b.equals(c)
,则a.equals(c)
- 与
hashCode()
一致性:两个相等的对象必须具有相同的hashCode()
值
Object
规范提供了一个模糊的准则,即equals()
和hashCode()
是一致的 -如果“不修改该对象的equals比较中使用的信息,则它们的结果对于以后的调用将是相同的”。 听起来有点像“计算的结果不应该改变,除非改变。” 这种模糊的陈述通常被解释为相等性和哈希值的计算应该是对象状态的确定性函数,而别无其他。
平等意味着什么?
Object类规范对equals()
和hashCode()
的要求非常容易遵循。 确定是否以及如何覆盖equals()
需要更多的判断。 对于简单的不可变值类,例如Integer
(实际上,几乎所有不可变类),选择都是显而易见的-相等性应基于基础对象状态的相等性。 对于Integer
,对象的唯一状态是基础整数值。
对于易变的物体,答案并不总是那么明确。 equals()
和hashCode()
应该基于对象的标识(如默认实现)还是基于对象的状态(如Integer和String)? 没有简单的答案-这取决于类的预期用途。 对于List
和Map
这样的容器,可以采用任何一种方法进行合理的论证。 Java类库中的大多数类(包括容器类hashCode()
在根据对象状态提供equals()
和hashCode()
实现方面都hashCode()
。
如果对象的hashCode()
值可以根据其状态更改,那么在将此类对象用作基于哈希的集合中的键时,我们必须小心,以确保在将其用作哈希键时,我们不允许更改它们的状态。 所有基于散列的集合均假定对象的散列值在用作集合的键时不会发生变化。 如果某个键的哈希码在集合中时发生更改,则可能会导致一些不可预测的混乱后果。 在实践中这通常不是问题-在HashMap
使用诸如List
类的可变对象作为键并不常见。
一个基于状态定义equals()
和hashCode()
的简单可变类的示例是Point
。 如果两个Point
对象引用相同的(x, y)
坐标,则它们是相等的,并且Point
的哈希值是从x
和y
坐标值的IEEE 754位表示形式得出的。
对于更复杂的类, equals()
和hashCode()
的行为甚至可以由超类或接口的规范强加。 例如,当且仅当另一个对象也是一个List
并且它们包含相同元素(由元素上的Object.equals()
定义Object.equals()
时, List
接口才要求List
对象等于另一个对象。 hashCode()
的要求定义得更加具体-列表的hashCode()
值必须符合以下计算:
hashCode = 1;
Iterator i = list.iterator();
while (i.hasNext()) {
Object obj = i.next();
hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
}
哈希值不仅取决于列表的内容,而且还指定了用于组合各个元素的哈希值的特定算法。 ( String
类指定了用于计算String
哈希值的类似算法。)
编写自己的equals()和hashCode()方法
覆盖默认的equals()
方法非常容易,但是要覆盖已经覆盖的equals()
方法在不违反对称性或传递性要求的情况下非常棘手。 覆盖equals()
,应始终在equals()
上包含一些Javadoc注释,以帮助那些可能想要扩展您的类的人正确地这样做。
作为一个简单的示例,请考虑以下类:
class A {
final B someNonNullField;
C someOtherField;
int someNonStateField;
}
我们将如何为此类编写equals()
方法? 这种方法适用于许多情况:
public boolean equals(Object other) {
// Not strictly necessary, but often a good optimization
if (this == other)
return true;
if (!(other instanceof A))
return false;
A otherA = (A) other;
return
(someNonNullField.equals(otherA.someNonNullField))
&& ((someOtherField == null)
? otherA.someOtherField == null
: someOtherField.equals(otherA.someOtherField)));
}
现在我们已经定义了equals()
,我们必须以兼容的方式定义hashCode()
。 定义hashCode()
一种兼容但不是所有有用的方法如下:
public int hashCode() { return 0; }
对于具有大量条目的HashMap
此方法将产生可怕的性能,但它确实符合规范。 一个更明智的执行hashCode()
的A
会是这样的:
public int hashCode() {
int hash = 1;
hash = hash * 31 + someNonNullField.hashCode();
hash = hash * 31
+ (someOtherField == null ? 0 : someOtherField.hashCode());
return hash;
}
请注意,这两种实现都将部分计算委托给类的状态字段的equals()
或hashCode()
方法。 根据您的类,您可能还希望将部分计算委托给超类的equals()
或hashCode()
函数。 对于原始字段,关联的包装器类中有帮助程序函数,可以帮助创建哈希值,例如Float.floatToIntBits
。
编写equals()
方法并非没有陷阱。 通常,在扩展本身覆盖equals()
的可实例化类时,完全重写equals()
是不切实际的,并且编写打算重写的equals()
方法(例如在抽象类中)与编写方法不同具体类的equals()
方法。 有关某些示例,请参见《 有效的Java编程语言指南》第7项,以及有关其原因的更多详细信息。
有改善的空间吗?
将散列构建到Java类库的根对象类中是一个非常明智的设计折衷方案-它使使用基于散列的容器变得更加容易和高效。 但是,对于Java类库中散列和相等性的方法和实现已提出了一些批评。 java.util
中基于散列的容器非常方便且易于使用,但可能不适用于需要非常高性能的应用程序。 尽管其中大多数将永远不会改变,但是当您设计严重依赖基于散列的容器的效率的应用程序时,请牢记这一点。 这些批评包括:
- 哈希范围太小。 将
int
而不是long
用作hashCode()
的返回类型会增加哈希冲突的可能性。 - 哈希值分布不正确。 短字符串和小整数的哈希值本身就是小整数,并且与其他“附近”对象的哈希值接近。 行为更完善的哈希函数将在哈希范围内更均匀地分配哈希值。
- 没有定义的哈希操作。 虽然某些类(例如
String
和List
)定义了一种哈希算法,用于将其组成元素的哈希值组合为一个哈希值,但语言规范并未定义将多个对象的哈希值组合为一个哈希值的任何认可方法一个新的哈希值。List
,String
或示例类A
先前在编写自己的equals()和hashCode()方法中讨论过的技巧很简单,但在数学上并不理想。 类库也没有提供任何哈希算法的便利实现,这些哈希算法会简化更复杂的hashCode()
实现的创建。 - 难度写作
equals()
扩展了新类时已经覆盖equals()
。 扩展已覆盖equals()
的可实例化类时,定义equals()
的“显而易见”方法都无法满足equals()
方法的对称性或可传递性要求。 这意味着您在覆盖equals()
时必须了解要扩展的类的结构和实现细节,甚至可能需要公开保护基类中的私有字段,从而违反了良好的面向对象设计原则。
摘要
通过一致地定义equals()
和hashCode()
,可以提高类作为基于哈希的集合中的键的可用性。 定义相等性和哈希值的方法有两种:基于身份的身份(这是Object
提供的默认值)和基于状态的身份,这两种方法都需要覆盖equals()
和hashCode()
。 如果对象的哈希值在状态更改时可以更改,请确保在将其用作哈希键时不允许其状态更改。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp05273/index.html