Java中HashCode()和equals()的作用
引言
我们知道Java中的集合(Collection)大致可以分为两类,一类是List,再有一类是Set。
前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。
这里就引出一个问题:要想保证元素不重复应该依据什么来判断呢?
为什么要用hashCode()?
为了解决放入重复数据的问题,一开始开发者们想到了用Object.equals方法。
但是,很快他们发现如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。
也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。
于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出“哈希算法”的概念,所以就以他的名字命名了。
哈希算法也称为散列算法,哈希值也称为散列码,实际上就是将数据依照哈希算法直接指定到一个地址上。
初学者可以简单理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。
这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。
如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。
所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
两个方法的作用
equals()作用:用于判断其他对象是否与该对象相同;
Object类中是这样定义equals():
public boolean equals(Object obj) { return (this == obj); }
很显然,在Object类原生代码中比较的是引用地址,但是需要提醒的一点是在String、Math、Integer、Double等封装类中都对equals()进行了不同程度的重写以满足其不同需要,例如在String类中:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = count; if (n == anotherString.count) { char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n– != 0) { if (v1[i++] != v2[j++]) return false; } return true; } } return false; }
显然,在String类中的equals()比较的不再是引用对象的地址而是内容,在Java8种基本数据类型中equals()比较的都是内容,其实就是数值。
HashCode()作用:给不同对象返回不同的hash code值,相当于识别码;
使用HashCode()时应当符合以下三点:
- 在一个Java应用的执行期间,如果一个对象提供给equals做比较的信息没有被修改的话,该对象无论调用多少次hashCode(),必须始终返回同一个integer;
- 如果两个对象根据equals(Object)方法是相等的,那么调用二者各自的hashCode()必须产生同一个integer结果;
- 调用equals(java.lang.Object)方法结果不相等的两个对象,调用二者各自hashCode()不一定不相同,可能相同,可能不同。
重点
在集合查找时,使用hashcode无疑能大大降低对象比较次数,提高查找效率!
Java对象的eqauls方法和hashCode方法是这样规定的:
1、相等(相同)的对象必须具有相等的哈希码(或者散列码)。
2、如果两个对象的hashCode相同,它们并不一定相同。
可能的困惑
一、相等(相同)的对象必须具有相等的哈希码(或者散列码),为什么?
假设A与B这两个对象相等,即他们equals的结果为true,但他们各自的哈希码不相同,但他们要存入同一个HashMap时,有可能就会因为哈希码不同导致计算得出的HashMap内部数组位置索引不一样,那么A、B很可能同时存入同一个HashMap中,但我们知道HashMap是不允许存放重复元素的。
二、为什么两个对象的hashCode相同他们也不一定相同?
你的问题其实也是在说不同对象的hashCode有可能相同,产生这种结果的原因我个人觉得是由于“哈希算法”在生产哈希码时造成的,两个对象在某些方面具有高度一致性。正因为考虑到可能会出现这样的情况,所以HashMap在添加两个hashCode完全相同的对象时会在此哈希码指定的内部数组的位置索引处建立一个新的链表,然后将两个对象串起来放在该位置,这样就能在保证虽然hashCode相同仍能存入HashMap中,当然前提是他们调用equals()的返回值为false。
再补充一点,在业界中有一个专门的术语去描述这种现象,我们称之为哈希冲突,很显然,虽然哈希冲突是可以解决的,但没有人会希望经常看到它。
实际操作
在实际编写代码程序时,我们经常会被要求重写hashCode()和equals(),曾经我也对这个问题百思不得其解,但现在我也能向大家解释这其中的秘密了。
以HashSet为例,我们知道HashSet是继承Set接口,而Set接口由实现了Collection接口,HashSet中不允许出现重复值,而且元素的位置也是不确定的。
那么在这里介绍一下Java集合判断两个对象是否相等的规则是:
1.首先要判断两个对象的hashCode是否相等;
如果相等,进入第二步再判断;
如果不相等,那么认为两个对象也不相等,结束判断。
2.判断两个对象用equals()是否相等。
如果这次判断也相等,则认为两个对象相等;
如果不相等,那么认为两个对象也不相等。
为什么要进行两次判断呢?
可以不进行第一次的判断,但如果没有,实际使用效率会大大降低,尤其是在进行大量数据比较时。其实前面在介绍hashCode()时有过提及,即hashCode()相等时,equals()也可能不相等,所以我们就加上了第二条判断进行限制。总的来说,就是可以没有第一条判断,但必须要有第二条判断,但在实际开发中两条都最好写上,一旦出现大量数据需要判断时,仅靠equals()进行判断的话执行效率会大打折扣。
代码展示
package Exercise;
import java.util.HashSet; public class e1 { public static void main(String[] args) { HashSet hs=new HashSet(); hs.add(new Student(1,"张三")); hs.add(new Student(2,"李四")); hs.add(new Student(3,"王麻子")); hs.add(new Student(1,"张三")); for (Object object : hs) { System.out.println(object); } } } class Student{ int num; String name; Student(int num,String name){ this.name=name; this.num=num; } public String toString(){ return num+":"+name; } }
运行结果:
为什么Hashset添加了相等的元素呢,这是不是和Hashset的原则违背了呢?回答是:没有。因为在根据hashCode()对两次建立的new Student(1,“张三 ”)对象进行比较时,生成的是不同的哈希码值,所以Hashset把他当作不同的对象对待了,当然此时的equals()方法返回的值也不等。
为什么会生成不同的哈希码值呢?原因就在于我们自己写的Student类并没有重新自己的hashCode()和equals()方法,所以在比较时,是继承的object类中的hashCode(),而object类中的hashCode()是一个本地方法,比较的是对象的地址(引用地址),使用new方法创建对象,两次生成的当然是不同的对象了,造成的结果就是两个对象的hashCode()返回的值不一样,所以Hashset会把它们当作不同的对象对待。
怎么解决这个问题呢?答案是:在Student类中重写hashCode()和equals()方法。
package Exercise; import java.util.HashSet; public class e1 { public static void main(String[] args) { HashSet hs=new HashSet(); hs.add(new Student(1,"张三")); hs.add(new Student(2,"李四")); hs.add(new Student(3,"王麻子")); hs.add(new Student(1,"张三")); for (Object object : hs) { System.out.println(object); } } } class Student{ int num; String name; Student(int num,String name){ this.name=name; this.num=num; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + num; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Student other = (Student) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (num != other.num) return false; return true; } public String toString(){ return num+":"+name; } }
运行结果
可以看到重复元素的问题已经消除,根据重写的方法,即便两次调用了new Student(1,"张三"),我们在获得对象的哈希码时,根据重写的方法hashCode(),获得的哈希码肯定是一样的,当然根据equals()方法我们也可判断是相同的,所以在向hashset集合中添加时把它们当作重复元素看待了。
刚才使用的重写是用快捷键进行的,我们也可以手敲,不过写这么多就没必要了。
手敲重写代码:
public int hashCode(){ return num * name.hashCode(); } public boolean equals(Object o){ Student s = (Student) o; return num == s.num && name.equals(s.name); }
做个总结
- 重点是equals,重写hashCode只是技术要求(为了提高效率);
- 为什么要重写equals呢?因为在Java的集合框架中,是通过equals来判断两个对象是否相等的;
- 在hibernate中,经常使用set集合来保存相关对象,而set集合是不允许重复的。在向HashSet集合中添加元素时,其实只要重写equals()这一条也可以。但当hashset中元素比较多时,或者是重写的equals()方法比较复杂时,我们只用equals()方法进行比较判断,效率也会非常低,所以引入了hashCode()这个方法,只是为了提高效率,且这是非常有必要的。
如果hashCode()这样写:
public int hashCode(){ return 1; //等价于hashcode无效 }
这样做的效果就是在比较哈希码的时候不能进行判断,因为每个对象返回的哈希码都是1,每次都必须要经过比较equals()方法后才能进行判断是否重复,这当然会引起效率的大大降低。
文章参考: