2019-06-17:谈谈如何重写equals()方法?为什么还要重写hashCode()?
Moosphan opened this issue · 9 comments
先来说一下hashcode()和equals方法吧。
hashcode()
- hashCode 的存在主要用于查找的快捷性,如 Hashtable, HashMap 等,hashCode 是用来在三列存储结构中确定对象的存储地址的。
- 如果两个对象相同,就是适用于 euqals(java.lang.Object) 方法,那么这两个对象的 hashCode一定相同。
- 如果对象的euqals 方法被重写,那么对象的 hashCode 也尽量重写,并且产生 hashCode 使用的对象,一定要和 equals 方法中使用的一致,否则就会违反上面提到的第二点。
- 两个对象的 hashCode 相同,并不一定表示这两个对象就相同,也就是不一定适用于equals() 方法,只能够说明这两个对象在三列存储结构中,如 Hashtable.,他们存在同一个篮子里。
以上话以前摘录自一篇博客,讲的非常好。
equals(Object obj)
- 如果一个类没有重写 equals(Object obj)方法,则等价于通过 == 比较两个对象,即比较的是对象在内存中的空间地址是否相等。
- 如果重写了equals(Object ibj)方法,则根据重写的方法内容去比较相等,返回 true 则相等,false 则不相等。
我用一个简单的demo来举个例子吧.
public class MyClass {
public static void main(String[] args) {
HashSet books=new HashSet();
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
System.out.println(books);
}
}
class A{
//类A的 equals 方法总是返回true,但没有重写其hashCode() 方法
@Override
public boolean equals(Object o) {
return true;
}
}
class B{
//类B 的hashCode() 方法总是返回1,但没有重写其equals()方法
@Override
public int hashCode() {
return 1;
}
}
class C{
public int hashCode(){
return 2;
}
@Override
public boolean equals(Object o) {
return true;
}
}
结果
- 即使两个A 对象通过 equals() 比较返回true,但HashSet 依然把他们当成 两个对象,即使两个 B 对象 的hashCode() 返回值相同,但HashSet 依然把他们当成两个对象。
- 即也就是,当把一个对象放入HashSet 中时,如果需要重写该对象对应类的 equals() 方法,则也应该重写其 hashCode() 方法。规则是:如果两个对象通过 equals() 方法比较返回true,这两个对象的 hashCode 值也应该相同。
- 如果两个对象通过euqals() 方法比较返回true,但这两个对象的 hashCode() 方法返回不同的hashCode 值时,这将导致HashSet 会把这两个对象保存在 Hash 表的不同位置,从而使两个对象都可以添加成功,这就与 Set 集合的规则冲突了。
- 如果两个对象的 hashCode() 方法返回的 hasCode 值相同,但他们通过 equals() 方法比较返回false 时将更麻烦:因为两个对象的hashCode 值相同,HashSet 将试图 把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet 访问集合元素时也是根据元素的 hashCode 值来快速定位的,如果 hashSet 中两个以上的元素具有相同的 HashCode 值时,将会导致性能下降。
附上我的这篇博客链接。
哈哈哈。
往HashMap添加元素的时候,需要先定位到在数组的位置(hashCode方法)。
如果只重写了 equals 方法,两个对象 equals 返回了true,集合是不允许出现重复元素的,只能插入一个。
此时如果没有重写 hashCode 方法,那么就无法定位到同一个位置,集合还是会插入元素。这样集合中就出现了重复元素了。那么重写的equals方法就没有意义了。
[如果重写了hashcode方法,确保两个对象都能够定位到相同的位置,那么就可以遍历这条单向链表,使用equals方法判断两个对象是否相同,如果相同,那么就不插入了(HashMap的实现仍然插入,但是覆盖掉旧的value)。如果不相同,就插入到链表的头节点处。
)
equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
所以我们比较两个对象相等一般先比较hashcode(效率高,不复杂),再使用equal比较
一般原则
理论:
首先要注意重写equals必须重写hashCode
(1.1)自反性:对于任何非null的引用值x,x.equals(x)=true
(1.2)对称性:对于任何非null的引用x,y,x.equals(y)=true,同样y.equals(y)=true
(1.3)传递性:对于任何非null的引用x,y,z,x.equals(y)=true,x.equals(z)=true =>y.equals(z)=true
(1.4)一致性:对于任何非null的引用x和y,只要equals的操作涉及的类信息没有改变,则x.equals(y)的结果一直不变;
对于任何为null的引用,null.equals(x)=false
具体做法:
(1)使用==操作符判断参数是否是这个对象的引用;
(2)使用instanceof操作符判断”参数是否是正确的类型“;
(3)把参数装换成正确的类型;
(4)对于参数中的各个域,判断其是否和对象中的域相匹配;
equals相等的hasCode肯定相等,hasCode相等的equals不一定相等
equals没有重写之前比较的是地址,重写后比较的是内容
往HashMap里面加入元素的时候,首先通过hasCode来判断一下是否有相同的元素
如果只有equals重写了,那么就会判断这两个元素相同,只能插入一个
如果这个时候没有重写hashCode,就没有办法定位到同一个位置,就会再重新插入一个位置,会出现重复元素。
如果重写了hashCode,就会定位到同一个位置,然后往后面的链表里面插入元素,然后通过equals判断两个元素是否相同,如果不相同就插入。
equals:比较两个对象的地址是否相等
hashCode :一般用在集合里面,比如在hashMap 里面,存入元素的时候 会首先算出 哈希值,然后根据哈希值来确定元素的位置,对于在任何一个对象上调用hashCode 时,返回的 哈希值一定相等的。
为什么 需要重写 hashCode
给集合中存元素时,首先会获取 hashCode 的值,如果没有重写 hashCode ,他会直接将元素的地址转换成一个整数返回。如果我们创建了两个对象,两个对象的所有属性值都一样,在存入HashSet 时,第一个元素会直接存进去,第二个获取的 哈希值 和 第一个不同,所以第二个元素也会存进去,因为 jdk 默认不同的 hashCode 值,equals 一定返回false。所以 这两个值都会被存进去。但是这两个对象的属性值都是一样的,所以这样会造成数据的不唯一性。所以一般重写了 equals 后必须要重写 hashCode。
内存泄露的问题
想象一下,一个类 创建了两个对象,属性值不同,同时重写了 equals 和 hashCode 。然后将他们都存进了 HashSet 中。然后修改第二个 元素的值。最后将第二个元素充 set 集合中删除。 删除之后 则迭代进行打印,会发现第二个元素没有被删除掉,为什么呢? 因为在删除 某个元素时,会获取 hashCode 值,但是由于修改了属性值,导致获取的 哈希值和 存入时获取的不同,所以查找为空,jdk 认为该对象不在集合中,所以不会进行删除操作,但是用户任务 对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。解决的办法是不要在执行的期间 修改与 HashCode 值相关的对象信息,如果非要修改,则必须先从集合中删除,更新数据后在添加到集合。
总结:
1.hashCode是为了提高在散列结构存储中查找的效率,在线性表中没有作用
2.equals和hashCode需要同时覆盖。
3.若两个对象equals返回true,则hashCode一定返回相同的int数。
4.若两个对象equals返回false,则hashCode不一定返回不同的int数,但为不相等的对象生成不同hashCode值可以提高 哈希表的性能
5.若两个对象hashCode返回相同int数,则equals不一定返回true。
6.若两个对象hashCode返回不同int数,则equals一定返回false。
7.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。
equals在内部调用"==",在不重写equals方法时,equals方法是比较两个对象是否具有相同的引用,即是否指向了同一个内存地址。而hashCode是一个本地方法,它返回的是这个对象的内存地址。
当重写equals方法时,就必须重写hashCode方法,如利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashCode值来进行判断是否相同的。这样如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但 不重写hashCode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashCode却是不一样的,由此将产生了不一致。
equals()方法比较的是对象的内存地址,而一般我们使用此方法意在比较两对象的在逻辑上是否相等,而不是两者的地址值,通常比较对象各个属性值是否相等,属性完全相等时在存储时,认为是一个对象,而hashcode有这样的规定:两个对象相等,hashCode一定相等。hashcode不等,两个对象一定不等。默认的hashcode 根据内存地址经过哈希算法实现的。比较两个对象,当重写的equals()计算两个对象完全相等,而两个对象的内存地址不相同,则计算得到的hashcode值不相等,出现矛盾。因此必须重写hashcode方法。
往HashMap添加元素的时候,需要先定位到在数组的位置(hashCode方法)。
如果只重写了 equals 方法,两个对象 equals 返回了true,集合是不允许出现重复元素的,只能插入一个。
此时如果没有重写 hashCode 方法,那么就无法定位到同一个位置,集合还是会插入元素。这样集合中就出现了重复元素了。那么重写的equals方法就没有意义了。
一、为什么重写了equals要重写hashcode?
有这么一个场景,当用户登录时,来了两个user,有name和age,并且还有手机号,手机号相同的我认为是一个用户。那么我们很容易得到以下代码:
import java.util.HashMap;
public class MockLogin {
static class User {
public int age;
public String name;
public String mobile;
public User( String mobile,int age, String name) {
this.age = age;
this.name = name;
this.mobile = mobile;
}
}
public static void main(String[] args) {
//小灰灰登录
User xhhFirstLogin = new User("123456",21, "xiaohuihui");
//这里我想记录小灰灰是否登陆过
HashMap<User,Boolean> map = new HashMap<>();
map.put(xhhFirstLogin,true);
//小灰灰第二次登陆来了,如果登陆过,我就把之前登陆信息给他,不再次登陆
User xhhSecondLogin = new User("123456",21, "xiaohuihui");
boolean b = map.containsKey(xhhSecondLogin);
//看是否登陆过
System.out.println(b?"登陆过":"没有登录");
}
}
但实际运行结果是:
没有登录
聪明的你一定看出来了,你没有重写equals啊,怎么判断手机号相等是同一个对象,好,那我们重写一下equals:
public class MockLogin {
static class User {
public int age;
public String name;
public String mobile;
public User( String mobile,int age, String name) {
this.age = age;
this.name = name;
this.mobile = mobile;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(mobile, user.mobile);
}
}
public static void main(String[] args) {
//小灰灰登录
User xhhFirstLogin = new User("123456",21, "xiaohuihui");
//这里我想记录小灰灰是否登陆过
HashMap<User,Boolean> map = new HashMap<>();
map.put(xhhFirstLogin,true);
//小灰灰第二次登陆来了,如果登陆过,我就把之前登陆信息给他,不再次登陆
User xhhSecondLogin = new User("123456",21, "xiaohuihui");
boolean b = map.containsKey(xhhSecondLogin);
//看是否登陆过
System.out.println(b?"登陆过":"没有登录");
}
}
重写完之后的结果:
没有登录
还是没有登录。那么问题究竟出在哪里了呢?既然是containsKey返回的false,我们就去看看containsKey是怎么写的吧
public boolean containsKey(Object key) {
return getNode(key) != null;
}
接着往下找:
/**
* Implements Map.get and related methods.
*
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
其实这里面只有一个行我们是需要注意的:
hash = hash(key));//根据key算出来的hash
if (first.hash == hash && //对比第一个节点的hash值
((k = first.key) == key || (key != null && key.equals(k)))) //如果key和第一个节点的key相同或者(key不等于空并且相等)
return first;
我们知道hashmap是一个拉链式的hash结构:
containsKey是先找到hashcode,然后再来对比是否相等的。那么我们很容易得到猜想,是不是刚才hashcode不一样,导致的containsKey返回了false。我们先来打印一下hashcode:
....
boolean b = map.containsKey(xhhSecondLogin);
System.out.println("fcode = "+xhhFirstLogin.hashCode()+" scode = "+xhhSecondLogin.hashCode());
看一下执行结果:
fcode = 1704856573 scode = 705927765
没有登录
果然,是hashcode导致了我们containsKey函数的失败。那我们先简单的处理一下,让他们相等:
public User( String mobile,int age, String name) {
this.age = age;
this.name = name;
this.mobile = mobile;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(mobile, user.mobile);
}
@Override
public int hashCode() {
return 1;
}
}
在user类增加hashcode方法,返回固定值1,我们再跑一下运行结果:
fcode = 1 scode = 1
登陆过
这时真的相等了,当然hashcode = 1这是为了做测试,工程化落地时,我们还是用hashcode标准方法吧:
@Override
public int hashCode() {
return Objects.hash(age, name, mobile);
}
我们认为,当他所有成员属性一样时,那就是一个对象,再次跑结果:
fcode = -679329960 scode = -679329960
登陆过
技术总结:
-
hashcode是一种快速定位到拉链法hash表数组位置索引的一个值,根据key算出来。
-
hashcode重写主要是为了解决对象在hash表中当key时,equals相等,但是hashcode不相等,导致containsKey错误返回的问题
贴一下最后的源代码:
package com.android;
import java.util.HashMap;
import java.util.Objects;
public class MockLogin {
static class User {
public int age;
public String name;
public String mobile;
public User( String mobile,int age, String name) {
this.age = age;
this.name = name;
this.mobile = mobile;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(mobile, user.mobile);
}
@Override
public int hashCode() {
return Objects.hash(age, name, mobile);
}
}
public static void main(String[] args) {
//小灰灰登录
User xhhFirstLogin = new User("123456",21, "xiaohuihui");
//这里我想记录小灰灰是否登陆过
HashMap<User,Boolean> map = new HashMap<>();
map.put(xhhFirstLogin,true);
//小灰灰第二次登陆来了,如果登陆过,我就把之前登陆信息给他,不再次登陆
User xhhSecondLogin = new User("123456",21, "xiaohuihui");
boolean b = map.containsKey(xhhSecondLogin);
System.out.println("fcode = "+xhhFirstLogin.hashCode()+" scode = "+xhhSecondLogin.hashCode());
//看是否登陆过
System.out.println(b?"登陆过":"没有登录");
}
}