|
![]() |
名片设计 CorelDRAW Illustrator AuotoCAD Painter 其他软件 Photoshop Fireworks Flash |
|
虽然Java语言不直接支持关联数组 -- 可以使用任何对象作为一个索引的数组 -- 但在根Object类中使用hashCode()方式明确表示期望广泛使用HashMap(及其前辈Hashtable)。理想情况下基于散列的容器提供有效插入和有效检索;直接在对象模式中支持散列可以促进基于散列的容器的开发和使用。 定义对象的相等性 Object类有两种方式来推断对象的标识:equals()和hashCode()。一般来说,假如您忽略了其中一种,您必须同时忽略这两种,因为两者之间有必须维持的至关重要的关系。特别情况是根据equals() 方式,假如两个对象是相等的,它们必须有一样的hashCode()值(尽管这通常不是真的)。 特定类的equals()的语义在Implementer的左侧定义;定义对特定类来说equals()意味着什么是其设计工作的一部分。Object提供的缺省实施简朴引用下面等式: public boolean equals(Object obj) { return (this == obj); } 在这种缺省实施情况下,只有它们引用真正同一个对象时这两个引用才是相等的。同样,Object提供的hashCode()的缺省实施通过将对象的内存地址对映于一个整数值来生成。由于在某些架构上,地址空间大于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中的要害字是切实可行的。这种基于值的Equal方式可以由Java类库中的所有原始封装类使用,如Integer、Float、Character和Boolean以及String(假如两个String对象包含一样顺序的字符,那它们是相等的)。由于这些类都是不可修改的并且可以实施hashCode()和equals(),它们都可以做为很好的散列要害字。 为什么忽略 equals()和hashCode()? 假如Integer不忽略equals() 和 hashCode()情况又将如何?假如我们从未在HashMap或其它基于散列的集合中使用Integer作为要害字的话,什么也不会发生。但是,假如我们在HashMap中使用这类Integer对象作为要害字,我们将不能够可靠地检索相关的值,除非我们在get()调用中使用与put()调用中极其类似的Integer实例。这要求确保在我们的整个程序中,只能使用对应于特定整数值的Integer对象的一个实例。不用说,这种方式极不方便而且错误频频。 Object的interface contract要求假如根据 equals()两个对象是相等的,那么它们必须有一样的hashCode()值。当其识别能力整个包含在equals()中时,为什么我们的根对象类需要hashCode()?hashCode()方式纯粹用于提高效率。Java平台设计人员预计到了典型Java应用程序中基于散列的集合类(Collection Class)的重要性--如Hashtable、HashMap和HashSet,并且使用equals()与许多对象进行比较在计算方面异常昂贵。使所有Java对象都能够支持 hashCode()并结合使用基于散列的集合,可以实现有效的存储和检索。 实施equals()和hashCode()的需求 实施equals()和 hashCode()有一些限制,Object文件中列举出了这些限制。特殊是equals()方式必须显示以下属性: Symmetry:两个引用,a和 b,a.equals(b) if and only if b.equals(a) Reflexivity:所有非空引用, a.equals(a) Transitivity:If a.equals(b) and b.equals(c), then a.equals(c) Consistency with hashCode():两个相等的对象必须有一样的hashCode()值 Object的规范中并没有明确要求equals()和 hashCode() 必须一致 -- 它们的结果在随后的调用中将是一样的,假设“不改变对象相等性比较中使用的任何信息。”这听起来象“计算的结果将不改变,除非实际情况如此。”这一模糊声明通常解释为相等性和散列值计算应是对象的可确定性功能,而不是其它。 对象相等性意味着什么? 人们很轻易满意Object类规范对equals() 和 hashCode() 的要求。决定是否和如何忽略equals()除了判定以外,还要求其它。在简朴的不可修值类中,如Integer(事实上是几乎所有不可修改的类),选择相称明显 -- 相等性应基于基本对象状态的相等性。在Integer情况下,对象的唯一状态是基本的整数值。 对于可修改对象来说,答案并不总是如此清晰。equals() 和hashCode() 是否应基于对象的标识(象缺省实施)或对象的状态(象Integer和String)?没有简朴的答案 -- 它取决于类的计划使用。对于象List和Map这样的容器来说,人们对此争论不已。Java类库中的大多数类,包括容器类,错误出现在根据对象状态来提供equals()和hashCode()实施。 假如对象的hashCode()值可以基于其状态进行更改,那么当使用这类对象作为基于散列的集合中的要害字时我们必须注重,确保当它们用于作为散列要害字时,我们并不答应更改它们的状态。所有基于散列的集合假设,当对象的散列值用于作为集合中的要害字时它不会改变。假如当要害字在集合中时它的散列代码被更改,那么将产生一些不可猜测和轻易混淆的结果。实践过程中这通常不是问题 -- 我们并不常常使用象List这样的可修改对象做为HashMap中的要害字。 一个简朴的可修改类的例子是Point,它根据状态来定义equals()和hashCode()。假如两个Point 对象引用一样的(x, y)座标,Point的散列值来源于x和y座标值的IEEE 754-bit表示,那么它们是相等的。 对于比较复杂的类来说,equals()和hashCode()的行为可能甚至受到superclass或interface的影响。例如,List接口要求假如并且只有另一个对象是List,而且它们有一样顺序的一样的Elements(由Element上的Object.equals() 定义),List对象等于另一个对象。hashCode()的需求更特别--list的hashCode()值必须符合以下计算: hashCode = 1; Iterator i = list.iterator(); while (i.hasNext()) { Object obj = i.next(); hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode()); } 不仅仅散列值取决于list的内容,而且还规定了结合各个Element的散列值的特别算法。(String类规定类似的算法用于计算String的散列值。) 编写自己的equals()和hashCode()方式 忽略缺省的equals()方式比较简朴,但假如不违背对称(Symmetry)或传递性(Transitivity)需求,忽略已经忽略的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; } 这种方式将生成大量的条目并显著降低HashMaps的性能,但它符合规范。一个更合理的hashCode()实施应该是这样: public int hashCode() { int hash = 1; hash = hash * 31 + someNonNullField.hashCode(); hash = hash * 31 + (someOtherField == null ? 0 : someOtherField.hashCode()); return hash; } 注重:这两种实施都降低了类状态字段的equals()或hashCode()方式一定比例的计算能力。根据您使用的类,您可能希望降低superclass的equals()或hashCode()功能一部分计算能力。对于原始字段来说,在相关的封装类中有helper功能,可以帮助创建散列值,如Float.floatToIntBits。 编写一个完美的equals()方式是不现实的。通常,当扩展一个自身忽略了equals()的instantiable类时,忽略equals()是不切实际的,而且编写将被忽略的equals()方式(如在抽象类中)不同于为详细类编写equals()方式。关于实例以及说明的更具体信息请参阅Effective Java Programming Language Guide, Item 7 (参考资料) 。 有待改进? 将散列法构建到Java类库的根对象类中是一种异常明智的设计折衷方式 -- 它使使用基于散列的容器变得如此简朴和高效。但是,人们对Java类库中的散列算法和对象相等性的方式和实施提出了许多批评。java.util中基于散列的容器异常方便和简便易用,但可能不适用于需要异常高性能的应用程序。虽然其中大部分将不会改变,但当您设计严峻依靠于基于散列的容器效率的应用程序时必须考虑这些因素,它们包括: 太小的散列范围。使用int而不是long作为hashCode()的返回类型增加了散列冲突的几率。 糟糕的散列值分配。短strings和小型integers的散列值是它们自己的小整数,接近于其它“邻近”对象的散列值。一个循规导矩(Well-behaved)的散列函数将在该散列范围内更均匀地分配散列值。 无定义的散列操作。虽然某些类,如String和List,定义了将其Element的散列值结合到一个散列值中使用的散列算法,但语言规范不定义将多个对象的散列值结合到新散列值中的任何批准的方式。我们在前面编写自己的equals()和hashCode()方式中讨论的List、String或实例类A使用的诀窍都很简朴,但算术上还远远不够完美。类库不提供任何散列算法的方便实施,它可以简化更先进的hashCode()实施的创建。 当扩展已经忽略了equals()的 instantiable类时很难编写equals()。当扩展已经忽略了equals()的 instantiable类时,定义equals()的“显而易见的”方法都不能满意equals()方式的对称或传递性需求。这意味着当忽略equals()时,您必须了解您正在扩展的类的结构和实施具体信息,甚至需要暴露基本类中的机密字段,它违背了面向对象的设计的原则。 结束语 通过统一定义equals()和hashCode(),您可以提升类作为基于散列的集合中的要害字的使用性。有两种方式来定义对象的相等性和散列值:基于标识,它是Object提供的缺省方式;基于状态,它要求忽略equals()和hashCode()。当对象的状态更改时假如对象的散列值发生变化,确信当状态作为散列要害字使用时您不答应更更改其状态。 返回类别: 教程 上一教程: Eclipse平台入门:开发环境与实例 下一教程: 创建 Java .exe 文件 您可以阅读与"Java 理论与实践: 哈希"相关的教程: · Java 理论与实践: 哈希 · Java 理论与实践: 关于非常的争论 · Java 理论与实践:变还是不变? · Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制 · Java 理论与实践: Web 层的状态复制 |
![]() ![]() |
快精灵印艺坊 版权所有 |
首页![]() ![]() ![]() ![]() ![]() ![]() ![]() |