一个很常见的错误根源在于没有改写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
分享到:
相关推荐
equals()和hashcode()这两个方法都是从object类中继承过来的。当String 、Math、还有Integer、Double。。。。等这些封装类在使用equals()方法时,已经覆盖了object类的equals()方法.
- 当重写 `equals()` 时,总是应该同时重写 `hashCode()`。 - 避免在 `equals()` 方法中使用 `==` 来比较对象,除非你想比较的是引用。 - 在 `hashCode()` 方法中,应使用对象的非空关键属性来计算哈希码,以保证...
在Java编程语言中,`hashCode()` 和 `equals()` 是两个非常重要的方法,它们主要用于对象的比较和哈希表(如HashMap)的操作。标题提到的"HashCode相同equals不同的2位字符集合算法"涉及到的是一个特定场景:两个...
Java重写equals同时需要重写hashCode的代码说明,以及如何重写hashCode方法,此代码演示按照effective java书籍说明的重写思路。代码中演示了使用集合存储对象,并且对象作为key,需重写equals和hashCode.
在Java编程语言中,`equals()`, `hashCode()` 和 `toString()` 是三个非常重要的方法,它们主要用于对象的比较、哈希存储以及打印对象信息。这三个方法是Java对象的基础特性,对于理解和开发高质量的Java程序至关...
2、为什么改写equals()的时候,总是要改写hashCode() 两个原则: hashCode()的返回值和equals()的关系如下: 如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。 如果x.equals(y)返回...
本文还介绍了定义对象的相等性、实施equals()和hashCode()的需求、编写自己的equals()和hashCode()方法。通过统一定义equals()和hashCode(),可以提升类作为基于散列的集合中的关键字的使用性。
在重写 `hashCode()` 时,要确保它和 `equals()` 方法保持一致。一种常见做法是将对象的各个属性值进行位运算(如异或、加法等),并结合对象的类的哈希码,以生成一个独特的整数。 关联 `equals()` 和 `hashCode()...
当同时重写 `equals()` 和 `hashCode()` 时,需要注意的一点是,如果 `equals()` 返回true,那么两个对象的 `hashCode()` 必须相等。反之则不一定,因为不同的对象可能会产生相同的哈希码。这就是为什么在设计类时,...
本文将详细介绍 hashCode() 和 equals() 的本质区别和联系,并探讨在创建 Java 类时如何定义这些方法。 hashCode() 方法 hashCode() 方法是 Object 类中的一个方法,它返回对象的哈希码值。哈希码值是一个整数,它...
equals 与 hashCode 方法讲解 equals 方法和 hashCode 方法是 Java 语言中两个重要的方法,它们都是在 Object 类中定义的。equals 方法用于比较两个对象是否相等,而 hashCode 方法用于返回对象的哈希码。 在 Java...
在Java编程语言中,`hashCode()` 和 `equals()` 方法是两个非常重要的概念,尤其是在处理对象比较和哈希表(如 `HashMap` 或 `HashSet`)时。这两个方法来源于 `Object` 类,是所有Java类的基类,因此,每个自定义类...
### hashCode和equals方法详解 #### 一、hashCode方法解析 在深入探讨`hashCode`方法之前,我们需要了解Java集合框架的基本概念。Java集合框架主要包括两大类集合:`List`和`Set`。 - **List**:这是一个有序集合...
### Java中`hashCode()`与`equals()`方法详解 #### 前言 在Java编程语言中,`hashCode()`和`equals()`方法是非常重要的概念,它们不仅对于深入理解Java内存管理至关重要,也是实现自定义类的关键部分之一。本文将...
### set接口中hashCode和equals方法详解 #### 一、引言 在Java编程语言中,`Set`接口作为集合框架的重要组成部分,在实现无重复元素的数据结构方面扮演着关键角色。为了确保元素的唯一性,`Set`接口依赖于对象的`...
在Java编程语言中,`equals()` 和 `hashCode()` 方法是两个非常重要的概念,尤其是在处理对象比较和哈希表(如 `HashMap` 和 `HashSet`)时。`equals()` 方法用于判断两个对象是否相等,而 `hashCode()` 方法则用于...
这样一来,当集合要添加新的元素时,先调用这个元素的 hashCode 方法,就一下子能定位到它应该放置的物理位置上。 五、equals 方法的实现机制 equals 方法的实现机制是基于内容比较的。equals 方法会比较两个对象...
在Java编程中,`equals()` 和 `hashCode()` 方法是Object类中的两个重要方法,它们在处理对象相等性以及在哈希表(如HashSet、HashMap)中起到关键作用。当自定义类时,有时需要根据业务逻辑重写这两个方法以满足...
在Java编程语言中,`hashCode()`和`equals()`方法是对象身份验证的关键组成部分,它们主要用于对象的比较和哈希表(如HashMap、HashSet等)的操作。理解这两个方法的工作原理对于编写高效和可靠的代码至关重要。 ...
深入解析Java对象的equals()和hashCode()的使用 在Java语言中,equals()和hashCode()两个函数的使用是紧密配合的,你要是自己设计其中一个,就要设计另外一个。在多数情况下,这两个函数是不用考虑的,直接使用它们...