zoukankan      html  css  js  c++  java
  • Hash表数据结构常识

    Hash表数据结构常识:
    一、哈希表基于数组。
    二、缺点:基于数组的,数组创建后难以扩展。某些哈希表被基本填满时,性能下降得非常严重。
    三、没有一种简便得方法可以以任何一种顺序遍历表中数据项。
    四、如果不需要有序遍历数据,并且可以提前预测数据量的大小,那么哈希表在速度和易用性方面是无与伦比的。
     
    一、为什么HashCode对于对象是如此的重要:
    一个对象的HashCode就是一个简单的Hash算法的实现,虽然它和那些真正的复杂的Hash算法相比还不能叫真正的算法,它如何实现它,不仅仅是程序员的编程水平问题,
    而是关系到你的对象在存取是性能的非常重要的关系.有可能,不同的HashCode可能会使你的对象存取产生,成百上千倍的性能差别.
    先来看一下,在JAVA中两个重要的数据结构:HashMap和Hashtable,虽然它们有很大的区别,如继承关系不同,对value的约束条件(是否允许null)不同,以及线程安全性等有着特定的区别,但从实现原理上来说,它们是一致的.所以,我们只以Hashtable来说明:
    在java中,存取数据的性能,一般来说当然是首推数组,但是在数据量稍大的容器选择中,Hashtable将有比数据性能更高的查询速度.具体原因看下面的内容.
    Hashtable在存储数据时,一般先将该对象的HashCode和0x7FFFFFFF做与操作,因为一个对象的HashCode可以为负数,这样操作后可以保证它为一个正整数.然后以Hashtable的长度取模,得到该对象在Hashtable中的索引.
    index = (o.hashCode() & 0x7FFFFFFF)%hs.length;
    这个对象就会直接放在Hashtable的每index位置,对于写入,这和数据一样,把一个对象放在其中的第index位置,但如果是查询,经过同样的算法,Hashtable可以直接从第index取得这个对象,而数组却要做循环比较.所以对于数据量稍大时,Hashtable的查询比数据具有更高的性能.
    既然一个对象可以根据HashCode直接定位它在Hashtable中的位置,那么为什么Hashtable还要用key来做映射呢?这就是关系Hashtable性能问题的最重要的问题:Hash冲突.
    常见的Hash冲突是不同对象最终产生了相同的索引,而一种非常甚至绝对少见的Hash冲突是,如果一组对象的个数大过了int范围,而HashCode的长度只能在int范围中,所以肯定要有同一组的元素有相同的HashCode,这样无论如何他们都会有相同的索引.当然这种极端的情况是极少见的,可以暂不考虑,但是对于同的HashCode经过取模,则会产中相同的索引,或者不同的对象却具有相同的HashCode,当然具有相同的索引.
    所以对于索引相同的对象,在该index位置存放了多个值,这些值要想能正确区分,就要依靠key来识别.
    事实上一个设计各好的HashTable,一般来说会比较平均地分布每个元素,因为Hashtable的长度总是比实际元素的个数按一定比例进行自增(装填因子一般为0.75)左右,这样大多数的索引位置只有一个对象,而很少的位置会有几个元素.所以Hashtable中的每个位置存放的是一个链表,对于只有一个对象是位置,链表只有一个首节点(Entry),Entry的next为null.然后有hashCode,key,value属性保存了该位置的对象的HashCode,key和value(对象本身),如果有相同索引的对象进来则会进入链表的下一个节点.如果同一个索引中有多个对象,根据 HashCode和key可以在该链表中找到一个和查询的key相匹配的对象.
    从上面我看可以看到,对于HashMap和Hashtable的存取性能有重大影响的首先是应该使该数据结构中的元素尽量大可能具有不同的HashCode,虽然这并不能保证不同的HashCode产生不同的 index,但相同的HashCode一定产生相同的index,从而影响产生Hash冲突.
    对于一个象,如果具有很多属性,把所有属性都参与散列,显然是一种笨拙的设计.因为对象的HashCode()方法几乎无所不在地被自动调用,如equals比较,如果太多的对象参与了散列.
    那么需要的操作常数时间将会增加很大.所以,挑选哪些属性参与散列绝对是一个编程水平的问题.
    从实现来说,一般的HashCode方法会这样:
    return Attribute1.HashCode() Attribute1.HashCode()..[ super.HashCode()],我们知道,每次调用这个方法,都要重新对方法内的参与散列的对象重新计算一次它们的HashCode的运算,如果一个对象的属性没有改变,仍然要每次都进行计算,所以如果设置一个标记来缓存当前的散列码,只要当参与散列的对象改变时才重新计算,否则调用缓存的 hashCode,这可以从很大程度上提高性能.
    默认的实现是将对象内部地址转化为整数作为HashCode,这当然能保证每个对象具有不同的HasCode,因为不同的对象内部地址肯定不同(废话),但java语言并不能让程序员获取对象内部地址,所以,让每个对象产生不同的HashCode有着很多可研究的技术.
    如果从多个属性中采样出能具有平均分布的hashCode的属性,这是一个性能和多样性相矛盾的地方,如果所有属性都参与散列,当然hashCode的多样性将大大提高,但牺牲了性能,而如果只能少量的属性采样散列,极端情况会产生大量的散列冲突,如对"人"的属性中,如果用性别而不是姓名或出生日期,那将只有两个或几个可选的hashcode值,将产生一半以上的散列冲突.所以如果可能的条件下,专门产生一个序列用来生成HashCode将是一个好的选择 (当然产生序列的性能要比所有属性参与散列的性能高的情况下才行,否则还不如直接用所有属性散列).
    如何对HashCode的性能和多样性求得一个平衡,可以参考相关算法设计的书,其实并不一定要求非常的优秀,只要能尽最大可能减少散列值的聚集.重要的是我们应该记得HashCode对于我们的程序性能有着重要的影响,在程序设计时应该时时加以注意.
    请记住:如果你想有效的使用HashMap,你就必须重写在其的HashCode()。
    还有两条重写HashCode()的原则:
    不必对每个不同的对象都产生一个唯一的hashcode,只要你的HashCode方法使get()能够得到put()放进去的内容就可以了。即“不为一原则”。生成hashcode的算法尽量使hashcode的值分散一些,不要很多hashcode都集中在一个范围内,这样有利于提高HashMap的性能。即“分散原则”。至于第二条原则的具体原因,有兴趣者可以参考 Bruce Eckel的《Thinking in Java》,
    在那里有对HashMap内部实现原理的介绍,这里就不赘述了。
    掌握 了这两条原则,你就能够用好HashMap编写自己的程序了。不知道大家注意没有, java.lang.Object中提供的三个方法:clone(),equals()和hashCode()虽然很典型,但在很多情况下都不能够适用,它们只是简单的由对象的地址得出结果。这就需要我们在自己的程序中重写它们,其实java类库中也重写了千千万万个这样的方法。利用面向对象的多态性—— 覆盖,Java的设计者很优雅的构建了Java的结构,也更加体现了Java是一门纯OOP语言的特性。
    Java提供的Collection和Map的功能是十分强大的,它们能够使你的程序实现方式更为灵活,执行效率更高。希望本文能够对大家更好的使用HashMap有所帮助。
     
    hashcode理论与实践:
    有效和正确定义hashCode()和equals()
    每个Java对象都有hashCode()和 equals()方法。许多类忽略(Override)这些方法的缺省实施,以在对象实例之间提供更深层次的语义可比性。
    虽 然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()。当对象的状态更改时如果对象的散列值发生变化,确信当状态作为散列关键字使用时您不允许更更改其状态。




    解析Java对象的equals()和hashCode()的使用:

    在 Java语言中,equals()和hashCode()两个函数的使用是紧密配合的,你要是自己设计其中一个,就要设计另外一个。在多数情况下,这两个函数是不用考虑的,直接使用它们的默认设计就可以了。但是在一些情况下,这两个函数最好是自己设计,才能确保整个程序的正常运行。最常见的是当一个对象被加入收集对象(collection object)时,这两个函数必须自己设计。更细化的定义是:如果你想将一个对象A放入另一个收集对象B里,或者使用这个对象A为查找一个元对象在收集对象B里位置的钥匙,并支持是否容纳,删除收集对象B里的元对象这样的操作,那么,equals()和hashCode()函数必须开发者自己定义。其他情况下,这两个函数是不需要定义的。

    equals():
    它是用于进行两个对象的比较的,是对象内容的比较,当然也能用于进行对象参阅值的比较。什么是对象参阅值的比较?就是两个参阅变量的值得比较,我们都知道参阅变量的值其实就是一个数字,这个数字可以看成是鉴别不同对象的代号。两个对象参阅值的比较,就是两个数字的比较,两个代号的比较。这种比较是默认的对象比较方式,在Object这个对象中,这种方式就已经设计好了。所以你也不用自己来重写,浪费不必要的时间。

    对象内容的比较才是设计equals()的真正目的,Java语言对equals()的要求如下,这些要求是必须遵循的。否则,你就不该浪费时间:
    对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。
    反射性:x.equals(x)必须返回是“true”。
    类推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。
    还有一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。
    任何情况下,x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。
    hashCode():
    这个函数返回的就是一个用来进行哈希操作的整型代号,请不要把这个代号和前面所说的参阅变量所代表的代号弄混了。后者不仅仅是个代号还具有在内存中才查找对象的位置的功能。hashCode()所返回的值是用来分类对象在一些特定的收集对象中的位置。这些对象是HashMap, Hashtable, HashSet,等等。这个函数和上面的equals()函数必须自己设计,用来协助HashMap, Hashtable, HashSet,等等对自己所收集的大量对象进行搜寻和定位。

    这些收集对象究竟如何工作的,想象每个元对象hashCode是一个箱子的编码,按照编码,每个元对象就是根据hashCode()提供的代号归入相应的箱子里。所有的箱子加起来就是一个HashSet,HashMap,或 Hashtable对象,我们需要寻找一个元对象时,先看它的代码,就是hashCode()返回的整型值,这样我们找到它所在的箱子,然后在箱子里,每个元对象都拿出来一个个和我们要找的对象进行对比,如果两个对象的内容相等,我们的搜寻也就结束。这种操作需要两个重要的信息,一是对象的 hashCode(),还有一个是对象内容对比的结果。

    hashCode()的返回值和equals()的关系如下:

    如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。
    如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。

    为 什么这两个规则是这样的,原因其实很简单,拿HashSet来说吧,HashSet可以拥有一个或更多的箱子,在同一个箱子中可以有一个或更多的独特元对象(HashSet所容纳的必须是独特的元对象)。这个例子说明一个元对象可以和其他不同的元对象拥有相同的hashCode。但是一个元对象只能和拥有同样内容的元对象相等。所以这两个规则必须成立。

    设计这两个函数所要注意到的:
    如果你设计的对象类型并不使用于收集性对象,那么没有必要自己再设计这两个函数的处理方式。这是正确的面向对象设计方法,任何用户一时用不到的功能,就先不要设计,以免给日后功能扩展带来麻烦。

    如果你在设计时想别出心裁,不遵守以上的两套规则,那么劝你还是不要做这样想入非非的事。我还没有遇到过哪一个开发者和我说设计这两个函数要违背前面说的两个规则,我碰到这些违反规则的情况时,都是作为设计错误处理。

    当一个对象类型作为收集型对象的元对象时,这个对象应该拥有自己处理equals(),和/或处理hashCode()的设计,而且要遵守前面所说的两种原则。equals()先要查null和是否是同一类型。查同一类型是为了避免出现ClassCastException这样的异常给丢出来。查 null是为了避免出现NullPointerException这样的异常给丢出来。

    如果你的对象里面容纳的数据过多,那么这两个函数 equals()和hashCode()将会变得效率低。如果对象中拥有无法serialized的数据,equals()有可能在操作中出现错误。想象一个对象x,它的一个整型数据是transient型(不能被serialize成二进制数据流)。然而equals()和hashCode()都有依靠这个整型数据,那么,这个对象在serialization之前和之后,是否一样?答案是不一样。因为serialization之前的整型数据是有效的数据,在serialization之后,这个整型数据的值并没有存储下来,再重新由二进制数据流转换成对象后,两者(对象在serialization 之前和之后)的状态已经不同了。这也是要注意的。


    ============================================================      
    有效和正确定义hashCode()和equals():
    ============================================================      

      级别:入门级


       每个Java对象都有hashCode()和 equals()方法。许多类忽略(Override)这些方法的缺省实施,以在对象实例之间提供更深层次的语义可比性。在Java理念和实践这一部分, Java开发人员Brian Goetz向您介绍在创建Java类以有效和准确定义hashCode()和equals()时应遵循的规则和指南。您可以在讨论论坛与作者和其它读者一同探讨您对本文的看法。(您还可以点击本文顶部或底部的讨论进入论坛。)
      
      虽然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()并结合使用基于散列的集合,可以实现有效的存储和检索。

    ==============================
    Go deep into HashCode:
    ==============================

    为什么HashCode对于对象是如此的重要?
    一个对象的HashCode就是一个简单的Hash算法的实现,虽然它和那些真正的复杂的
    Hash算法相比还不能叫真正的算法,但如何实现它,不仅仅是程序员的编程水平问题,
    而是关系到你的对象在存取时性能的非常重要的问题.有可能,不同的HashCode可能
    会使你的对象存取产生,成百上千倍的性能差别.

    我们先来看一下,在JAVA中两个重要的数据结构:HashMap和Hashtable,虽然它们有很
    大的区别,如继承关系不同,对value的约束条件(是否允许null)不同,以及线程安全性
    等有着特定的区别,但从实现原理上来说,它们是一致的.所以,我们只以Hashtable来
    说明:

    在java中,存取数据的性能,一般来说当然是首推数组,但是在数据量稍大的容器选择中,
    Hashtable将有比数据性能更高的查询速度.具体原因看下面的内容.

    Hashtable在存储数据时,一般先将该对象的HashCode和0x7FFFFFFF做与操作,因为一个
    对象的HashCode可以为负数,这样操作后可以保证它为一个正整数.然后以Hashtable的
    长度取模,得到该对象在Hashtable中的索引.

    index = (o.hashCode() & 0x7FFFFFFF)%hs.length;
    这个对象就会直接放在Hashtable的第index位置,对于写入,这和数组一样,把一个对象
    放在其中的第index位置,但如果是查询,经过同样的算法,Hashtable可以直接从第index
    取得这个对象,而数组却要做循环比较.所以对于数据量稍大时,Hashtable的查询比数据
    具有更高的性能.

    既然可以根据HashCode直接定位对象在Hashtable中的位置,那么为什么Hashtable
    要用key来做映射呢(为了一些思维有障碍的人能看到懂我加了一句话:而不是直接放value呢)
    ?这就是关系Hashtable性能问题的最重要的问题:Hash冲突.

    常见的Hash冲突是不同对象最终产生了相同的索引,而一种非常甚至绝对少见的Hash冲突
    是,如果一组对象的个数大过了int范围,而HashCode的长度只能在int范围中,所以肯定要
    有同一组的元素有相同的HashCode,这样无论如何他们都会有相同的索引.当然这种极端
    的情况是极少见的,可以暂不考虑,但对于相同的HashCode经过取模,则会产中相同的索引,
    或者不同的对象却具有相同的HashCode,当然具有相同的索引.

    所以对于索引相同的对象,在该index位置存放了多个对象,这些值要想能正确区分,就要依
    靠key本身和hashCode来识别.

    事实上一个设计各好的HashTable,一般来说会比较平均地分布每个元素,因为Hashtable
    的长度总是比实际元素的个数按一定比例进行自增(装填因子一般为0.75)左右,这样大多
    数的索引位置只有一个对象,而很少的位置会有几个对象.所以Hashtable中的每个位置存
    放的是一个链表,对于只有一个对象的位置,链表只有一个首节点(Entry),Entry的next为
    null.然后有hashCode,key,value. 属性保存了该位置的对象的HashCode,key和value(对象
    本身),如果有相同索引的对象进来则会进入链表的下一个节点.如果同一个位置中有多个
    对象,根据HashCode和key可以在该链表中找到一个和查询的key相匹配的对象.

    从上面我看可以看到,对于HashMap和Hashtable的存取性能有重大影响的首先是应该使该
    数据结构中的元素尽量大可能具有不同的HashCode,虽然这并不能保证不同的HashCode
    产生不同的index,但相同的HashCode一定产生相同的index,从而影响产生Hash冲突.

    对于一个象,如果具有很多属性,把所有属性都参与散列,显然是一种笨拙的设计.因为对象
    的HashCode()方法几乎无所不在地被自动调用,如equals比较,如果太多的对象参与了散列.
    那么需要的操作常数时间将会增加很大.所以,挑选哪些属性参与散列绝对是一个编程水平
    的问题.

    从实现来说,一般的HashCode方法会这样:

    return Attribute1.HashCode() + Attribute2.HashCode()...[+super.HashCode()],

    我们知道,每次调用这个方法,都要重新对方法内的参与散列的对象重新计算一次它们的
    HashCode的运算,如果一个对象的属性没有改变,仍然要每次都进行计算,所以如果设置一
    个标记来缓存当前的散列码,只要当参与散列的对象改变时才重新计算,否则调用缓存的
    hashCode,这可以从很大程度上提高性能.


    默认的实现是将对象内部地址转化为整数作为HashCode,这当然能保证每个对象具有不同
    的HasCode,因为不同的对象内部地址肯定不同(废话),但java语言并不能让程序员获取对
    象内部地址,所以,让每个对象产生不同的HashCode有着很多可研究的技术.

    如何从多个属性中采样出能具有多样性的hashCode的属性,这是一个性能和多样性相矛
    盾的地方,如果所有属性都参与散列,当然hashCode的多样性将大大提高,但牺牲了性能,
    而如果只有少量的属性采样散列,极端情况会产生大量的散列冲突,如对"人"的属性中,如
    果用性别而不是姓名或出生日期,那将只有两个或几个可选的hashcode值,将产生一半以上
    的散列冲突.所以如果可能的条件下,专门产生一个序列用来生成HashCode将是一个好的选
    择(当然产生序列的性能要比所有属性参与散列的性能高的情况下才行,否则还不如直接用
    所有属性散列).

    如何对HashCode的性能和多样性求得一个平衡,可以参考相关算法设计的书,其实并不一定
    要求非常的优秀,只要能尽最大可能减少散列值的聚集.重要的是我们应该记得HashCode对
    于我们的程序性能有着生要的影响,在程序设计时应该时时加以注意.

  • 相关阅读:
    C#递规与分治策略
    SuperMap Objects Java & Applet
    如何提高显示速度
    系统测试
    ora01033:oracle initialization or shutdown in progress
    ORA12535: TNS:operation timed out。
    oralce01033
    hsql初体验
    创建Oracle数据源失败
    转载地图优化
  • 原文地址:https://www.cnblogs.com/wzfyang/p/3229954.html
Copyright © 2011-2022 走看看