重写equals方法的时候为什么需要重写hashcode

原作地址:https://www.jianshu.com/p/75d9c2c3d0c1

困扰我很久的问题,一直不明白为什么重写equals()方法的时候要重写hashCode()方法,这次总算弄明白了,作此分享,如有不对之处,望大家指正。

一、equals()方法

先说说equals()方法。
  查看Java的Object.equals()方法,如下:

public boolean equals(Object object){
      return(this == obj);
}

可以看到这里直接用'=='来直接比较,引用《Java编程思想》里的一句话:“关系操作符生成的是一个boolean结果,它们计算的是操作数的值之间的关系”。那么'=='比较的值到底是什么呢?
  我们知道Java有8种基本类型:数值型(byte、short、int、long、float、double)、字符型(char)、布尔型(boolean),对于这8种基本类型的比较,变量存储的就是值,所以比较的就是'值'本身。如下,值相等就是true,不等就是false。

public static void main(String[] args) {  
        int a=3;                                           
        int b=4;
        int c=3;
        System.out.println(a==b);   //false
        System.out.println(a==c);   //true
    }

对于非基本类型,也就是常说的引用数据类型:类、接口、数组,由于变量种存储的是内存中的地址,并不是'值'本身,所以真正比较的是该变量存储的地址,可想而知,如果声明的时候是2个对象,地址固然不同。

public static void main(String[] args) {
        String str1 = new String("123");
        String str2 = new String("123");
        System.out.println(str1 == str2);  //false
    }

可以看到,上面这种比较方法,和Object类中的equals()方法的具体实现相同,之所以为false,是因为直接比较的是str1和str2指向的地址,也就是说Object中的equals方法是直接比较的地址,因为Object类是所有类的基类,所以调用新创建的类的equals方法,比较的就是两个对象的地址。那么就有人要问了,如果就是想要比较引用类型实际的值是否相等,该如何比较呢?
    铛铛铛...... 重点来了


要解决上面的问题,就是今天要说的equals(),具体的比较由各自去重写,比较具体的值的大小。我们可以看看上面字符串的比较,如果调用String的equals方法的结果。

public static void main(String[] args) {
        String str1 = new String("123");
        String str2 = new String("123");
        System.out.println(str1.equals(str2));  //true
    }

可以看到返回的true,由兴趣的同学可以去看String equals()的源码。


所以可以通过重写equals()方法来判断对象的值是否相等,但是有一个要求:equals()方法实现了等价关系,即:

  • 自反性:对于任何非空引用x,x.equals(x)应该返回true;
  • 对称性:对于任何引用x和y,如果x.equals(y)返回true,那么y.equals(x)也应该返回true;
  • 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true;
  • 一致性:如果x和y引用的对象没有发生变化,那么反复调用x.equals(y)应该返回同样的结果;
  • 非空性:对于任意非空引用x,x.equals(null)应该返回false;

二、hashCode()方法

此方法返回对象的哈希码值,什么是哈希码?度娘找到的相关定义:

哈希码产生的依据:哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。也有相同的情况,看程序员如何写哈希码的算法。

简单理解就是一套算法算出来的一个值,且这个值对于这个对象相对唯一。哈希算法有一个协定:在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行hashcode比较时所用的信息没有被修改。(ps:要是每次都返回不一样的,就没法玩儿了)

public static void main(String[] args) {
        List<Long> test1 = new ArrayList<Long>();
        test1.add(1L);
        test1.add(2L);
        System.out.println(test1.hashCode());  //994
        test1.set(0,2L);
        System.out.println(test1.hashCode());  //1025
    }

三、标题解答

首先来看一段代码:

public class HashMapTest {
    private int a;

    public HashMapTest(int a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
        HashMapTest instance = new HashMapTest(1);
        map.put(instance, 1);
        Integer value = map.get(new HashMapTest(1));
        if (value != null) {
            System.out.println(value);
        } else {
            System.out.println("value is null");
        }
    } 

}
//程序运行结果: value is null

简单说下HashMap的原理,HashMap存储数据的时候,是取的key值的哈希值,然后计算数组下标,采用链地址法解决冲突,然后进行存储;取数据的时候,依然是先要获取到hash值,找到数组下标,然后for遍历链表集合,进行比较是否有对应的key。比较关心的有2点:1.不管是put还是get的时候,都需要得到key的哈希值,去定位key的数组下标; 2.在get的时候,需要调用equals方法比较是否有相等的key存储过。
  反过来,我们再分析上面那段代码,Map的key是我们自己定义的一个类,可以看到,我们没有重写equal方法,更没重写hashCode方法,意思是map在进行存储的时候是调用的Object类中equals()和hashCode()方法。为了证实,我们打印下hashCode码。

public class HashMapTest {
    private Integer a;

    public HashMapTest(int a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
        HashMapTest instance = new HashMapTest(1);
        System.out.println("instance.hashcode:" + instance.hashCode());
        map.put(instance, 1);
        HashMapTest newInstance = new HashMapTest(1);
        System.out.println("newInstance.hashcode:" + newInstance.hashCode());
        Integer value = map.get(newInstance);
        if (value != null) {
            System.out.println(value);
        } else {
            System.out.println("value is null");
        }
    }
}
//运行结果:
//instance.hashcode:929338653
//newInstance.hashcode:1259475182
//value is null

不出所料,hashCode不一致,所以对于为什么拿不到数据就很清楚了。这2个key,在Map计算的时候,可能数组下标就不一致,就算数据下标碰巧一致,根据前面,最后equals比较的时候也不可能相等(很显然,这是2个对象,在堆上的地址必定不一样)。我们继续往下看,假如我们重写了equals方法,将这2个对象都put进去,根据map的原理,只要是key一样,后面的值会替换前面的值,接下来我们实验下:

public class HashMapTest {
    private Integer a;

    public HashMapTest(int a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
        HashMapTest instance = new HashMapTest(1);
        HashMapTest newInstance = new HashMapTest(1);
        map.put(instance, 1);
        map.put(newInstance, 2);
        Integer value = map.get(instance);
        System.out.println("instance value:"+value);
        Integer value1 = map.get(newInstance);
        System.out.println("newInstance value:"+value1);

    }

    public boolean equals(Object o) {
        if(o == this) {
            return true;
        } else if(!(o instanceof HashMapTest)) {
            return false;
        } else {
            HashMapTest other = (HashMapTest)o;
            if(!other.canEqual(this)) {
                return false;
            } else {
                Integer this$data = this.getA();
                Integer other$data = other.getA();
                if(this$data == null) {
                    if(other$data != null) {
                        return false;
                    }
                } else if(!this$data.equals(other$data)) {
                    return false;
                }

                return true;
            }
        }
    }
    protected boolean canEqual(Object other) {
        return other instanceof HashMapTest;
    }

    public void setA(Integer a) {
        this.a = a;
    }

    public Integer getA() {
        return a;
    }
}
//运行结果:
//instance value:1
//newInstance value:2

你会发现,不对呀?同样的一个对象,为什么在map中存了2份,map的key值不是不能重复的么?没错,它就是存的2份,只不过在它看来,这2个的key是不一样的,因为他们的哈希码就是不一样的,可以自己测试下,上面打印的hash码确实不一样。那怎么办?只有重写hashCode()方法,更改后的代码如下:

public class HashMapTest {
    private Integer a;

    public HashMapTest(int a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Map<HashMapTest, Integer> map = new HashMap<HashMapTest, Integer>();
        HashMapTest instance = new HashMapTest(1);
        System.out.println("instance.hashcode:" + instance.hashCode());
        HashMapTest newInstance = new HashMapTest(1);
        System.out.println("newInstance.hashcode:" + newInstance.hashCode());
        map.put(instance, 1);
        map.put(newInstance, 2);
        Integer value = map.get(instance);
        System.out.println("instance value:"+value);
        Integer value1 = map.get(newInstance);
        System.out.println("newInstance value:"+value1);

    }

    public boolean equals(Object o) {
        if(o == this) {
            return true;
        } else if(!(o instanceof HashMapTest)) {
            return false;
        } else {
            HashMapTest other = (HashMapTest)o;
            if(!other.canEqual(this)) {
                return false;
            } else {
                Integer this$data = this.getA();
                Integer other$data = other.getA();
                if(this$data == null) {
                    if(other$data != null) {
                        return false;
                    }
                } else if(!this$data.equals(other$data)) {
                    return false;
                }

                return true;
            }
        }
    }
    protected boolean canEqual(Object other) {
        return other instanceof HashMapTest;
    }

    public void setA(Integer a) {
        this.a = a;
    }

    public Integer getA() {
        return a;
    }

    public int hashCode() {
        boolean PRIME = true;
        byte result = 1;
        Integer $data = this.getA();
        int result1 = result * 59 + ($data == null?43:$data.hashCode());
        return result1;
    }
}
//运行结果:
//instance.hashcode:60
//newInstance.hashcode:60
//instance value:2
//newInstance value:2

可以看到,他们的hash码是一致的,且最后的结果也是预期的。


完美的分界线

ps.总结:对于这个问题,是比较容易被忽视的,曾经同时趟过这坑,Map中存了2个数值一样的key,所以大家谨记哟! 在重写equals方法的时候,一定要重写hashCode方法。
最后一点:有这个要求的症结在于,要考虑到类似HashMap、HashTable、HashSet的这种散列的数据类型的运用。



作者:程序员的散漫生活
链接:https://www.jianshu.com/p/75d9c2c3d0c1
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后附上Object和String的equal和hashcode方法供参考:

Object:

equals

public boolean equals(Object obj)
指示其他某个对象是否与此对象“相等”。

equals 方法在非空对象引用上实现相等关系

  • 自反性:对于任何非空引用值 xx.equals(x) 都应返回 true
  • 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true
  • 传递性:对于任何非空引用值 xy 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true
  • 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
  • 对于任何非空引用值 xx.equals(null) 都应返回 false

Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 truex == y 具有值 true)。

注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

参数:
obj - 要与之比较的引用对象。
返回:
如果此对象与 obj 参数相同,则返回  true;否则返回  false

hashCode

public int hashCode()
返回该对象的哈希码值。支持此方法是为了提高哈希表(例如  java.util.Hashtable  提供的哈希表)的性能。

hashCode 的常规协定是:

  • 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
  • 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)

返回:
此对象的一个哈希码值。

String(均重写了):

equals

public boolean equals(Object anObject)
将此字符串与指定的对象比较。当且仅当该参数不为  null,并且是与此对象表示相同字符序列的  String 对象时,结果才为  true

覆盖:
类  Object 中的  equals
参数:
anObject - 与此  String 进行比较的对象。
返回:
如果给定对象表示的  String 与此 String 相等 ,则返回  true ;否则返回  false
另请参见:
compareTo(String)equalsIgnoreCase(String)

hashCode

public int hashCode()
返回此字符串的哈希码。  String  对象的哈希码根据以下公式计算:
 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
 
使用  int  算法,这里  s[i]  是字符串的第  i  个字符,  n  是字符串的长度,  ^  表示求幂。(空字符串的哈希值为 0。)

覆盖:
类  Object 中的  hashCode
返回:
此对象的哈希码值。
另请参见:
Object.equals(java.lang.Object)Hashtable


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值