Java 进阶 & hashCode与equals详解
在了解hashCode与equals方法之前,需要知道:
1、JAVA八种基本数据类型的变量没有equals和hashcode方法,只能应用==进行比较;
2、变量1==变量2,比较的是对象在堆内存中的地址,如果要比较其中的内容的话,就要用equals方法(前提是在自己定义的对象中重写了equals方法,如果没有重写equals方法使用equals比较和==的意义是相同的,都是比较对象存放的地址,因为Object 的equals方法,就是用==来实现的)。
学习hashCode与equals方法之前先来试想一个场景,如果你想查找一个集合中是否包含某个对象,那么程序应该怎么写呢?
通常的做法是逐一取出每个元素与要查找的对象一 一比较,当发现两者进行equals比较结果相等时,则停止查找并返回true,否则,返回false。但是这个做法的一个缺点是在需要进行大量并且快速的对象对比时,如果都用equals()去做显然效率太低。于是有人发明了一种哈希算法来提高从该集合中查找元素的效率,这种方式将集合分成若干个存储区域(可以看成一个个桶),每个对象可以计算出一个哈希码,可以根据哈希码分组,每组分别对应某个存储区域,这样一个对象根据它的哈希码就可以分到不同的存储区域(不同的桶中)。如下图所示:
image所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equals()去再对比了)。如果hashCode()相同,此时再对比他们的equals(),如果equals()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性!
实际的使用中,一个对象一般有key和value,可以根据key来计算对象的hashCode。假设现在全部的对象都已经根据自己的hashCode值存储在不同的存储区域中了,那么查找某个对象(根据对象的key来查找),不需要遍历整个集合了,现在只需要计算要查找对象的key的hashCode,然后找到该hashCode对应的存储区域,在该存储区域中来查找就可以了。
这种大量的并且快速的对象对比的使用,一般是在hash容器中,比如HashSet,HashMap,HashTable等。比如HashSet里对象无序且不可重复,当添加对象时,它内部必然要对添加进去的每个对象进行对比,而它的对比规则就是像上面说的那样,首先比较两个对象的hashcode值,如果hashcode值相同,则再通过equals比较,再相同的话才能确定是同一个对象,不添加。否则就可以添加进HashSet。 而HashMap,通过一个object的key来取HashMap的value时,工作方法是,通过传入的Object的hashCode在内存中找地址,当找到这个地址后再通过equals方法来比较这个地址中的内容是否和你原来放进去的一样,如果是,就取出value。
这样一来,对比的效率就很高了。
那么hashCode()既然效率这么高为什么还要equals()呢?
因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以可以得出:
1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
2.hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。
下面就来看看hashCode和equals的区别和联系。
在研究这个问题之前,首先说明一下JDK对equals(Object obj)和hashCode()这两个方法的定义和规范。
在Java中任何一个对象都具备equals(Object obj)和hashCode()这两个方法,因为他们是在Object类中定义的。
equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。
hashCode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。
举例说明一下:
两个Man对象,存储内容一致,都没有重写hashcode和equals方法
Man man = new Man("liming", 25, "lili");
Man man1 = new Man("liming", 25, "lili");
Set<Man> set = new HashSet<Man>();
set.add(man);
set.add(man1);
System.out.println(set.size());
输出:2
两个man对象,存储内容不一致,但是自定义了hashcode和equals比较规则
@Override
public int hashCode()
{
// TODO Auto-generated method stub
return 123;
}
@Override
public boolean equals(Object obj)
{
Man man = (Man) obj;
return man.age == this.age;
}
Man man = new Man("liming", 25, "lili");
Man man1 = new Man("limingasd", 25, "liliasd");
Set<Man> set = new HashSet<Man>();
set.add(man);
set.add(man1);
System.out.println(set.size());
输出:1
下面是官方文档给出的一些说明:
hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
查阅了相关资料之后对以上的说明做的归纳总结:
1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
2.若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。
3.若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数。
4.若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。
5.若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。
6.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。
想要弄清楚以上六点,先要知道什么时候需要重写equals和hashCode。一般来说涉及到对象之间的比较大小就需要重写equals方法,但是为什么第一点说重写了equals就需要重写hashCode呢?实际上这只是一条规范,如果不这样做程序也可以执行,只不过会隐藏bug。一般一个类的对象如果会存储在HashTable,HashSet,HashMap等散列存储结构中,那么重写equals后最好也重写hashCode,否则会导致存储数据的不唯一性(存储了两个equals相等的数据)。而如果确定不会存储在这些散列结构中,则可以不重写hashCode。但是个人觉得还是重写比较好一点,谁能保证后期不会存储在这些结构中呢,况且重写了hashCode也不会降低性能,因为在线性结构(如ArrayList)中是不会调用hashCode,所以重写了也不要紧,也为后期的修改打了补丁。
对象放入散列集合的流程图:
image从上面的图中可以清晰地看到在存储一个对象时,先进行hashCode值的比较,然后进行equals的比较。
我们还可以通过JDK中得源码来认识一下具体hashCode和equals在代码中是如何调用的。
HashSet中add方法源代码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
map.put源代码:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
最后再来看几个测试的例子吧:
测试案例一:覆盖equals(Object obj)但不覆盖hashCode(),导致数据不唯一性
public class HashCodeTest {
public static void main(String[] args) {
Collection set = new HashSet();
Point p1 = new Point(1, 1);
Point p2 = new Point(1, 1);
System.out.println(p1.equals(p2));
set.add(p1); //(1)
set.add(p2); //(2)
set.add(p1); //(3)
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
System.out.println(object);
}
}
}
class Point {
private int x;
private int y;
public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Point other = (Point) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
@Override
public String toString() {
return "x:" + x + ",y:" + y;
}
}
输出结果:
true
x:1,y:1
x:1,y:1
原因分析:
(1)当执行set.add(p1)时(1),集合为空,直接存入集合;
(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,因为没有覆盖hashCode方法,所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与p2相同hashCode值的对象,因此jdk默认不同hashCode值,equals一定返回false,所以直接存入集合。
(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,继续判断equals是否返回true,因为是同一对象所以返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。
测试案例二:覆盖hashCode方法,但不覆盖equals方法,仍然会导致数据的不唯一性
修改Point类:
class Point {
private int x;
private int y;
public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public String toString() {
return "x:" + x + ",y:" + y;
}
}
输出结果:
false
x:1,y:1
x:1,y:1
原因分析:
(1)当执行set.add(p1)时(1),集合为空,直接存入集合;
(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,这里覆盖了hashCode方法,p1和p2的hashCode相等,所以继续判断equals是否相等,因为这里没有覆盖equals,默认使用'=='来判断,所以这里equals返回false,jdk认为是不同的对象,所以将p2存入集合。
(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,并且equals返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。
综合上述两个测试,要想保证元素的唯一性,必须同时覆盖hashCode和equals才行。
(注意:在HashSet中插入同一个元素(hashCode和equals均相等)时,会被舍弃,而在HashMap中插入同一个Key(Value 不同)时,原来的元素会被覆盖。)
测试案例三:在内存泄露问题
public class HashCodeTest {
public static void main(String[] args) {
Collection set = new HashSet();
Point p1 = new Point(1, 1);
Point p2 = new Point(1, 2);
set.add(p1);
set.add(p2);
p2.setX(10);
p2.setY(10);
set.remove(p2);
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
System.out.println(object);
}
}
}
class Point {
private int x;
private int y;
public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Point other = (Point) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
@Override
public String toString() {
return "x:" + x + ",y:" + y;
}
}
运行结果:
x:1,y:1
x:10,y:10
原因分析:
假设p1的hashCode为1,p2的hashCode为2,在存储时p1被分配在1号桶中,p2被分配在2号筒中。这时修改了p2中与计算hashCode有关的信息(x和y),当调用remove(Object obj)时,首先会查找该hashCode值得对象是否在集合中。假设修改后的hashCode值为10(仍存在2号桶中),这时查找结果空,jdk认为该对象不在集合中,所以不会进行删除操作。然而用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。解决该问题的办法是不要在执行期间修改与hashCode值有关的对象信息,如果非要修改,则必须先从集合中删除,更新信息后再加入集合中。
总结:
1、equals方法用于比较对象的内容是否相等(覆盖以后)
2、hashCode是为了提高在散列结构存储中查找的效率,只有在集合中用到,在线性表中没有作用。
3、equals和hashCode需要同时覆盖。
4、若两个对象equals返回true,则hashCode有必要也返回相同的int数
5、若两个对象equals返回false,则hashCode不一定返回不同的int数,但为不相等的对象生成不同hashCode值可以提高哈希表的性能。
6、若两个对象hashCode返回相同int数,则equals不一定返回true。
7、若两个对象hashCode返回不同int数,则equals一定返回false。
8、同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。
参考文章: