Moosphan/Android-Daily-Interview

2019-06-17:谈谈如何重写equals()方法?为什么还要重写hashCode()?

Moosphan opened this issue · 9 comments

2019-06-17:谈谈如何重写equals()方法?为什么还要重写hashCode()?

先来说一下hashcode()和equals方法吧。

hashcode()

  1. hashCode 的存在主要用于查找的快捷性,如 Hashtable, HashMap 等,hashCode 是用来在三列存储结构中确定对象的存储地址的。
  2. 如果两个对象相同,就是适用于 euqals(java.lang.Object) 方法,那么这两个对象的 hashCode一定相同。
  3. 如果对象的euqals 方法被重写,那么对象的 hashCode 也尽量重写,并且产生 hashCode 使用的对象,一定要和 equals 方法中使用的一致,否则就会违反上面提到的第二点。
  4. 两个对象的 hashCode 相同,并不一定表示这两个对象就相同,也就是不一定适用于equals() 方法,只能够说明这两个对象在三列存储结构中,如 Hashtable.,他们存在同一个篮子里。
    以上话以前摘录自一篇博客,讲的非常好。

equals(Object obj)

  1. 如果一个类没有重写 equals(Object obj)方法,则等价于通过 == 比较两个对象,即比较的是对象在内存中的空间地址是否相等。
  2. 如果重写了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;
 }
}

结果

  1. 即使两个A 对象通过 equals() 比较返回true,但HashSet 依然把他们当成 两个对象,即使两个 B 对象 的hashCode() 返回值相同,但HashSet 依然把他们当成两个对象。
  2. 即也就是,当把一个对象放入HashSet 中时,如果需要重写该对象对应类的 equals() 方法,则也应该重写其 hashCode() 方法。规则是:如果两个对象通过 equals() 方法比较返回true,这两个对象的 hashCode 值也应该相同。
  3. 如果两个对象通过euqals() 方法比较返回true,但这两个对象的 hashCode() 方法返回不同的hashCode 值时,这将导致HashSet 会把这两个对象保存在 Hash 表的不同位置,从而使两个对象都可以添加成功,这就与 Set 集合的规则冲突了。
  4. 如果两个对象的 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结构:

9_1_hashcode_and_equals

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?"登陆过":"没有登录");
    }
}