`
TonyLian
  • 浏览: 402016 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

【第8条】改写equals时总是要改写hashCode

阅读更多

    一个很常见的错误根源在于没有改写hashCode方法。在每一个改写了equals的方法的类中,你必须也要改写hashCode方法。如果不这么做的话,就会违反Object.hashCode的通用约定,从而导致该类无法与所有基于散列值(hash)的集合类结合在一起正常运行,这样的集合类包括HashMap、HashSet、Hashtable。


    可能有人会问:什么是hash?它是干什么用的?


    的确,一开始我也没有在意到hashCode。翻了一下底层API的代码,我自己的类确实也犯了这个错误,而且还发现其他项目组的API也是。那个项目是从Java1.0版本就做起的,直到现在使用的是1.5,但是他们说了:我们改写了equals的类只是一些JavaBean,他们很少有机会(不太严谨的说是从不会)被放到HashMap、HashSet、Hashtable中,就更不会放进去而且还当Key用了。


    那么,还是回答一下刚才的问题吧,什么是hash?它是干什么用的?如果违反了此条到底在什么情况下,会有怎样的后果?


    hash是一个散列值,或者叫“杂乱编码”,对一个对象做hashCode()运算,简单地说就是给这个对象算出一个无规则的ID。我们知道,数组的优势在于遍历,而HashMap等的优势在于快速查找。当我们在HashMap中放入了一些Key-Vaule的值对后,可以通过Key值很快地检索出它所对应的Value(很像对数据库表的查询),关键的是这个检索的时间耗费是固定的(严谨地说应该是与容量无关的),而非与内容多少线性的。很明显它不是像数组一样循环遍历、比较来做查找的,而是直达目标。那么它是如何做到的呢?


    这里再多啰嗦几句,HashMap的原理:当你初始化一个HashMap时,系统会预先开创一些空间用于放置将要被放入的对象,之后随着对象的放入,当容量不够用的时候就将容积扩倍。但是,什么时候叫“不够用”呢?并不是现有的“格子”都被放满了,而是75%(这个百分比叫loadFactor,Java用的是0.75,其它系统可能不一样,微软好像是0.72)。这是为什么呢?其实这个百分比是可以自己指定的,但是如果没有特别的情况,建议你不要这么做,相信很多数学专家已经经过大量的计算和论证才选用的0.75这个值的。再多说一句,其实所谓“扩倍”也是不严格的说法,各个系统在选择容量上都有自己的策略,比如Java是不小于扩倍数的一个奇数,而微软好像是用质数。再有就是容量的初始值各系统也不相同,Java是11。


    当一个Key-Value值对被put进来时,首先计算Key的hashCode,然后对容量取余,这个余数就是这个值对将要被存放的位置。get的时候,也是首先对Key算hashCode,然后对容量取余数,之后直接到余数的位置就找的目标了(那么如果在set和get之间,这个HashMap扩容过,那么该如何呢?这就不在这里详细讨论了)。这就是为什么检索的时间耗费是与容量无关的了。但是,两个不同的对象也可能具有相同的hashCode,或者两个具有不同hashCode的对象恰巧它们的HashCode之差是容量的整数倍,这样都会导致它们得到的余数是相同的,就又怎么办呢?HashMap会在每个位置上其实都是一个链表,如果有两个以上的对象落在了相同的位置上,那么就让链表上第一个元素的next指向下一个元素即可(如果某位置上仅有一个元素,那它的next就是null),这就是为什么严谨地说不能是“时间耗费是固定的”,在链表上查找还是需要时间一个一个的遍历比较的。现在我们知道了吧,即使约定并不要求通过equals方法判断是不相等的两个对象的hashCode也一定不能相同,但是最好还是让他们不同的好。如果这样一个hashCode()算法:

 

public int hashCode(){ 
    return 42; 
}

 
    虽然不是错误的,也满足约定,但它是从来不该出现的,如果采用这样的hashCode()方法的类的对象被装入HashMap的话,它们的位置会都在一处,也就是成了一个链表了。
   
    现在知道了为什么一定要改写hashCode,该将如何改写了。书上又给出了一个“处方”:
    1)int result = 17;
    2)对每个关键域(我觉得就是那些影响equals的域)如下处理:
      2.1)int c; 并根据该域的类型:
       2.1.1)如果该域f是boolean型,c = (f ? 0 : 1);
       2.1.2)如果该域f是byte、char、short、int型,c = (int)f;
       2.1.3)如果该域f是long型,c = (f ^ (f >>> 32));
       2.1.4)如果该域f是float型,c = Float.floatToIntBits(f);
       2.1.5)如果该域f是double型,c = (int)Double.doubleToLongBits(f);
       2.1.6)如果该域f是一个对象,c = f.hashCode();
       2.1.7)如果该域f是一个数组,遍历数组的每个元素,并按2.2)中的做法吧这些散列值组合起来;
      2.2)result = 37 * result + c
    3)return result
   
    例子:
    我们还是看在【第1条】出现的那个复数的例子,如果该类没有改写hashCode方法,不能满足Object的规范:
    如果两个对象根据equals(Object)方法是相等的,那么调用其中任一个对象的hashCode方法必须产生同样的整数结果。
    则会出现这样的尴尬:

    Complex c1 = new Complex(15.5,10.2);
    Map hm = new HashMap();
    hm.put(c1, "c1");

      

    之后如果你想检查某一个用户输入的复数,是否在这个HashMap中,而恰巧用户输入的re和im值正好是15.5和10.2

    String cName = hm.get(new Complex(re, im));  //返回null

 

    却返回的是null,而并非所设想的"c1"。原因就是两次new Complex(15.5,10.2)的hashCode不是同一个整数。虽然在这个例子中,可以通过两次都用 Complex.valueOf(15.5,10.2) 来代替 new Complex(15.5,10.2),或者将Complex类做成非可变类,从而得到正确的结果(为什么?如果不清楚,请看【第1条】【第13条】),但这个例子还是要告诉你改写hashCode的必要性。
   
    那么Complex类的hashCode方法按照“处方”应该写成这样:

    public int hashCode(){
      int result = 17;
      result = 37 * result + Float.floatToIntBits(this.re);
      result = 37 * result + Float.floatToIntBits(this.im);
      return result;
    }

 

 

 

【Effective Java 学习笔记】系列连载专题请见:
http://tonylian.iteye.com/categories/64208

 

1
0
分享到:
评论
1 楼 lxh2002 2009-05-12  
写得不错,分析都挺详细的。

相关推荐

    hashcode和equals方法

    equals()和hashcode()这两个方法都是从object类中继承过来的。当String 、Math、还有Integer、Double。。。。等这些封装类在使用equals()方法时,已经覆盖了object类的equals()方法.

    重写equals和hashcode方法_equals_重写equals和hashcode方法_

    - 当重写 `equals()` 时,总是应该同时重写 `hashCode()`。 - 避免在 `equals()` 方法中使用 `==` 来比较对象,除非你想比较的是引用。 - 在 `hashCode()` 方法中,应使用对象的非空关键属性来计算哈希码,以保证...

    HashCode相同equals不同的2位字符集合算法

    在Java编程语言中,`hashCode()` 和 `equals()` 是两个非常重要的方法,它们主要用于对象的比较和哈希表(如HashMap)的操作。标题提到的"HashCode相同equals不同的2位字符集合算法"涉及到的是一个特定场景:两个...

    Java重写equals同时需要重写hashCode的代码说明

    Java重写equals同时需要重写hashCode的代码说明,以及如何重写hashCode方法,此代码演示按照effective java书籍说明的重写思路。代码中演示了使用集合存储对象,并且对象作为key,需重写equals和hashCode.

    equals,hashcode,toString

    在Java编程语言中,`equals()`, `hashCode()` 和 `toString()` 是三个非常重要的方法,它们主要用于对象的比较、哈希存储以及打印对象信息。这三个方法是Java对象的基础特性,对于理解和开发高质量的Java程序至关...

    Java中equals()与hashCode()的原理与设计

     2、为什么改写equals()的时候,总是要改写hashCode()  两个原则:  hashCode()的返回值和equals()的关系如下:  如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。  如果x.equals(y)返回...

    Java理论与实践:hashCode()和equals()方法

    本文还介绍了定义对象的相等性、实施equals()和hashCode()的需求、编写自己的equals()和hashCode()方法。通过统一定义equals()和hashCode(),可以提升类作为基于散列的集合中的关键字的使用性。

    equals与hashCode在实际开发中的重写写法

    在重写 `hashCode()` 时,要确保它和 `equals()` 方法保持一致。一种常见做法是将对象的各个属性值进行位运算(如异或、加法等),并结合对象的类的哈希码,以生成一个独特的整数。 关联 `equals()` 和 `hashCode()...

    Java_重写equals()和hashCode()

    当同时重写 `equals()` 和 `hashCode()` 时,需要注意的一点是,如果 `equals()` 返回true,那么两个对象的 `hashCode()` 必须相等。反之则不一定,因为不同的对象可能会产生相同的哈希码。这就是为什么在设计类时,...

    关于hashCode()和equals()的本质区别和联系

    本文将详细介绍 hashCode() 和 equals() 的本质区别和联系,并探讨在创建 Java 类时如何定义这些方法。 hashCode() 方法 hashCode() 方法是 Object 类中的一个方法,它返回对象的哈希码值。哈希码值是一个整数,它...

    equals与hashCode方法讲解

    equals 与 hashCode 方法讲解 equals 方法和 hashCode 方法是 Java 语言中两个重要的方法,它们都是在 Object 类中定义的。equals 方法用于比较两个对象是否相等,而 hashCode 方法用于返回对象的哈希码。 在 Java...

    hashcode()和equals()

    在Java编程语言中,`hashCode()` 和 `equals()` 方法是两个非常重要的概念,尤其是在处理对象比较和哈希表(如 `HashMap` 或 `HashSet`)时。这两个方法来源于 `Object` 类,是所有Java类的基类,因此,每个自定义类...

    hashcode和equals的分析

    ### hashCode和equals方法详解 #### 一、hashCode方法解析 在深入探讨`hashCode`方法之前,我们需要了解Java集合框架的基本概念。Java集合框架主要包括两大类集合:`List`和`Set`。 - **List**:这是一个有序集合...

    java中hashcode()和equals()方法详解

    ### Java中`hashCode()`与`equals()`方法详解 #### 前言 在Java编程语言中,`hashCode()`和`equals()`方法是非常重要的概念,它们不仅对于深入理解Java内存管理至关重要,也是实现自定义类的关键部分之一。本文将...

    set接口经常用的hashCode和equals方法详解

    ### set接口中hashCode和equals方法详解 #### 一、引言 在Java编程语言中,`Set`接口作为集合框架的重要组成部分,在实现无重复元素的数据结构方面扮演着关键角色。为了确保元素的唯一性,`Set`接口依赖于对象的`...

    java 基础之(equals hashcode)

    在Java编程语言中,`equals()` 和 `hashCode()` 方法是两个非常重要的概念,尤其是在处理对象比较和哈希表(如 `HashMap` 和 `HashSet`)时。`equals()` 方法用于判断两个对象是否相等,而 `hashCode()` 方法则用于...

    java中hashcode和equals的详解.pdf

    这样一来,当集合要添加新的元素时,先调用这个元素的 hashCode 方法,就一下子能定位到它应该放置的物理位置上。 五、equals 方法的实现机制 equals 方法的实现机制是基于内容比较的。equals 方法会比较两个对象...

    重写hashCode()和equals()方法详细介绍

    在Java编程中,`equals()` 和 `hashCode()` 方法是Object类中的两个重要方法,它们在处理对象相等性以及在哈希表(如HashSet、HashMap)中起到关键作用。当自定义类时,有时需要根据业务逻辑重写这两个方法以满足...

    java中hashcode()和equals()的详解

    在Java编程语言中,`hashCode()`和`equals()`方法是对象身份验证的关键组成部分,它们主要用于对象的比较和哈希表(如HashMap、HashSet等)的操作。理解这两个方法的工作原理对于编写高效和可靠的代码至关重要。 ...

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

    深入解析Java对象的equals()和hashCode()的使用 在Java语言中,equals()和hashCode()两个函数的使用是紧密配合的,你要是自己设计其中一个,就要设计另外一个。在多数情况下,这两个函数是不用考虑的,直接使用它们...

Global site tag (gtag.js) - Google Analytics