• 为什么重写equals方法,还必须要重写hashcode方法,重写equals()和hashCode()方法实例


    Java中的hash值主要是用来在散列存储结构中确定对象的存储地址的,提高对象的查询效率。

    Java设计的顶级父类Object类中,有两个方法很特殊,它们分别是equals方法与hashCode方法。——一旦重写了equals方法,就一定要重写hashCode方法。以下是Object的源码:

    1. public class Object {
    2. /*
    3. * Note that it is generally necessary to override the {@code hashCode}
    4. * method whenever this method is overridden, so as to maintain the
    5. * general contract for the {@code hashCode} method, which states
    6. * that equal objects must have equal hash codes.
    7. */
    8. public boolean equals(Object obj) {
    9. return (this == obj);
    10. }
    11. /*
    12. * The general contract of {@code hashCode} is:
    13. *
      • *
      • Whenever it is invoked on the same object more than once during
    14. * an execution of a Java application, the {@code hashCode} method
    15. * must consistently return the same integer, provided no information
    16. * used in {@code equals} comparisons on the object is modified.
    17. * This integer need not remain consistent from one execution of an
    18. * application to another execution of the same application.
    19. *
    20. If two objects are equal according to the {@code equals(Object)}
  • * method, then calling the {@code hashCode} method on each of
  • * the two objects must produce the same integer result.
  • *
  • It is not required that if two objects are unequal
  • * according to the {@link java.lang.Object#equals(java.lang.Object)}
  • * method, then calling the {@code hashCode} method on each of the
  • * two objects must produce distinct integer results. However, the
  • * programmer should be aware that producing distinct integer results
  • * for unequal objects may improve the performance of hash tables.
  • *
  • *

  • * As much as is reasonably practical, the hashCode method defined by
  • * class {@code Object} does return distinct integers for distinct
  • * objects. (This is typically implemented by converting the internal
  • * address of the object into an integer, but this implementation
  • * technique is not required by the
  • * Java™ programming language.)
  • *
  • */
  • public int hashCode() {
  • return identityHashCode(this);
  • }
  • /* package-private */ static int identityHashCode(Object obj) {
  • int lockWord = obj.shadow$_monitor_;
  • final int lockWordStateMask = 0xC0000000; // Top 2 bits.
  • final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
  • final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits.
  • if ((lockWord & lockWordStateMask) == lockWordStateHash) {
  • return lockWord & lockWordHashMask;
  • }
  • return identityHashCodeNative(obj);
  • }
  • }

  • 如果一个类没有重写equals(Object obj)方法,则等价于通过==比较两个对象,即比较的是对象在内存中的空间地址是否相等;如果重写了equals(Object obj)方法,则根据重写的方法内容去比较相等,返回true则相等,false则不相等。

    equals方法注释中的大致意思是:当我们将equals方法重写后有必要将hashCode方法也重写,这样做才能保证不违背hashCode方法中“相同对象必须有相同哈希值”的约定。

    hashCode方法本质就是一个哈希函数,Object类的作者在注释的最后一段的括号中写道:将对象的地址值映为integer类型的哈希值。

    提到hashCode,就会想到哈希表,将某一对象的值映射到表中的某个位置,从而达到以O(1)的时间复杂度来查询该对象的值。hashCode是一个native方法,哈希值的计算利用的内存地址。哈希表在一定程度上也可以起到判重的作用,但也可能存储哈希冲突,即使是两个不同的对象,它们的哈希值也可能是相同的。因此,虽然哈希表具有优越的查询性能,但也可能存在哈希冲突。

    hashCode方法注释中列了个列表,列表中有三条注释,当前需要理解的大致意思如下:


    图中存在两种独立的情况:

    相同的对象必然导致相同的哈希值;
    不同的哈希值必然是由不同对象导致的;
    因此,equals方法与hashCode方法根本就是配套使用的。对于任何一个对象,不论是使用继承自Object的equals方法还是重写equals方法。hashCode方法实际上必须要完成的一件事情就是,为该equals方法认定为相同的对象返回相同的哈希值。如果只重写equals方法没有重写hashCode方法,就会导致``equals`认定相同的对象却拥有不同的哈希值。

    Object类中的equals方法区分两个对象的做法是比较地址值,即使用==。如果根据业务需求改写了equals方法的实现,那么也应当同时改写hashCode方法的实现。否则hashCode方法依然返回的是依据Object类中的依据地址值得到的integer哈希值。

    代入到具体的例子 –String类,在String类中,equals方法经过重写,具体实现源码如下:

    1. public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    2. public boolean equals(Object anObject) {
    3. if (this == anObject) {
    4. return true;
    5. }
    6. if (anObject instanceof String) {
    7. String anotherString = (String) anObject;
    8. int n = length();
    9. if (n == anotherString.length()) {
    10. int i = 0;
    11. while (n-- != 0) {
    12. if (charAt(i) != anotherString.charAt(i))
    13. return false;
    14. i++;
    15. }
    16. return true;
    17. }
    18. }
    19. return false;
    20. }
    21. }


    通过源码可以看到,String对象在调用equals方法比较另一个对象时,除了认定相同地址值的两个对象相等以外,还认定对应着的每个字符都相等的两个String对象也相等,即使这两个String对象的地址值不同(即属于两个对象)。

    String类中对equals方法进行重写扩充了,但是如果此时我们不将hashCode方法也进行重写,那么String类调用的就是来自顶级父类Obejct类中的hashCode方法。即,对于两个字符串对象,使用它们各自的地址值映射为哈希值。 也就是会出现如下情形:

    也就是说,被String类中的equals方法认定为相等的两个对象拥有两个不同的哈希值——因为它们的地址值不同。

    为什么重写equals方法就得重写hashCode方法?—— 因为必须保证重写后的equals方法认定相同的两个对象拥有相同的哈希值。同时我们也得出了——hashCode方法的重写原则就是保证equals方法认定为相同的两个对象拥有相同的哈希值

    equals里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么既然hashCode效率这么高为什么还要使用equals进行比较呢?

    因为hashCode并不是完全可靠,有时候不同的对象生成的hashcode也会一样(hash冲突),所以hashCode只能说是大部分时候可靠,并不是绝对可靠。 所以可以得出:

    equals相等的两个对象,它们的hashCode肯定相等,也就是用equals对比是绝对可靠的;
    hashCode相等的两个对象,它们的equals不一定相等,也就是hashCode不是绝对可靠的;
    所有对于需要大量并且快速的对比的话如果都用equals去做显然效率太低,解决方式是,每当需要对比的时候, hashCode去对比,这就用到了哈希表,能够快速的地位到对象的存储位置,如果hashCode不一样,则表示这两个对象肯定不相等(也就是不必再用equals去再对比了),如果hashCode相同,此时再对比它们的 equals,如果equals也相同,则表示这两个对象是真的相同了。

    查看字符串的hashCode:

    1. String str = new String("abc");
    2. System.out.println(str.hashCode()); // 96354
    3. str += "a";
    4. System.out.println(str.hashCode()); // 2987071
    5. str += "b";
    6. System.out.println(str.hashCode()); // 92599299
    7. String str1 = "abcab";
    8. System.out.println(str.hashCode() + " " + str1.hashCode()); // 92599299 92599299
    9. String str2 = "ab";
    10. String str3 = new String("ab");
    11. System.out.println(str2.hashCode() + " " + str3.hashCode()); // 3105 3105


    以下是String.hashCode的源码:

    1. public final class String implements java.io.Serializable, Comparable, CharSequence {
    2. private int hash; // Default to 0
    3. /**
    4. * Returns a hash code for this string. The hash code for a
    5. * {@code String} object is computed as
    6. *
    7. * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
    8. *
  • * using {@code int} arithmetic, where {@code s[i]} is the
  • * ith character of the string, {@code n} is the length of
  • * the string, and {@code ^} indicates exponentiation.
  • * (The hash value of the empty string is zero.)
  • *
  • * @return a hash code value for this object.
  • */
  • public int hashCode() {
  • int h = hash;
  • if (h == 0 && value.length > 0) {
  • char val[] = value;
  • for (int i = 0; i < value.length; i++) {
  • h = 31 * h + val[i];
  • }
  • hash = h;
  • }
  • return h;
  • }
  • }
  • 【面试】hashCode与equals两者之间的关系 / == 和equals / 为什么要重写equals方法 / 重写equals /hashcode方法 / 为什么要重写hashCode方法
    pdf 0星 超过10%的资源 63KB
    下载


    由上面的代码可知,String类的hashCode的值为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],其中s是字符串对应的char数组,所以字符串内容一样的String对象,调用hasCode()返回值是一样的。但是反过来,当hashCode返回的值一样时,其字符串内容不一定一样,因为上面的方法计算hash时可能会发生位数溢出。
     

    每个覆盖了equals方法的类中,必须覆盖hashCode。如果不这么做,就违背了hashCode的通用约定,也就是上面注释中所说的。进而导致该类无法结合所以与散列的集合一起正常运作,这里指的是HashMap、HashSet、HashTable、ConcurrentHashMap。
    来自 Effective Java 第三版

    结论:如果重写equals不重写hashCode它与散列集合无法正常工作。

    既然这样那我们就拿我们最熟悉的HashMap来进行演示推导吧。我们知道HashMap中的key是不能重复的,如果重复添加,后添加的会覆盖前面的内容。那么我们看看HashMap是如何来确定key的唯一性的。

    1. static final int hash(Object key) {
    2. int h;
    3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    4. }

    查看代码发现,它是通过计算Map key的hashCode值来确定在链表中的存储位置的。那么这样就可以推测出,如果我们重写了equals但是没重写hashCode,那么可能存在元素重复的矛盾情况。

    下面我们来演示一下

    1. public class Employee {
    2. private String name;
    3. private Integer age;
    4. public Employee(String name, Integer age) {
    5. this.name = name;
    6. this.age = age;
    7. }
    8. @Override
    9. public boolean equals(Object o) {
    10. if (this == o) return true;
    11. if (o == null || getClass() != o.getClass()) return false;
    12. Employee employee = (Employee) o;
    13. return Objects.equals(name, employee.name) &&
    14. Objects.equals(age, employee.age);
    15. }
    16. /*@Override
    17. public int hashCode() {
    18. return Objects.hash(name, age);
    19. }*/
    20. }
    1. public static void main(String[] args) {
    2. Employee employee1 = new Employee("冰峰", 20);
    3. Employee employee2 = new Employee("冰峰", 22);
    4. Employee employee3 = new Employee("冰峰", 20);
    5. HashMap<Employee, Object> map = new HashMap<>();
    6. map.put(employee1, "1");
    7. map.put(employee2, "1");
    8. map.put(employee3, "1");
    9. System.out.println("equals:" + employee1.equals(employee3));
    10. System.out.println("hashCode:" + (employee1.hashCode() == employee3.hashCode()));
    11. System.out.println(JSONObject.toJSONString(map));
    12. }

    按正常情况来推测,map中值存在两个元素,employee2和employee3。

    执行结果

    出现这种问题的原因就是因为没有重写hashCode,导致map在计算key的hash值的时候,绝对值相同的对象计算除了不一致的hash值。


    重写hashCode()和equals()方法详细介绍
    pdf 0星 超过10%的资源 107KB
    下载

    接下来我们打开hashCode的注释代码,看看执行结果

    总结

    如果重写了equals就必须重写hashCode,如果不重写将引起与散列集合(HashMap、HashSet、HashTable、ConcurrentHashMap)的冲突。

    ​​​​​​​

    重写实例

    重写equals必需重写hashCode。

    两个值hashCode值相等,这两不一定相等。

    但是两个equals值相等,hashCode一定相等

    idea 

    alt+insert可以生成两个方法,依次改动

    1. @Override
    2. public boolean equals(Object o) {
    3. if (this == o) return true;
    4. if (o == null || getClass() != o.getClass()) return false;
    5. TextImplicaMarkedDatasetText that = (TextImplicaMarkedDatasetText) o;
    6. //就是比对是不是相等
    7. Set<String> set = markedList.stream().map(TextImpMarkedHypot::getText).collect(Collectors.toSet());
    8. Set<String> set2 = that.markedList.stream().map(TextImpMarkedHypot::getText).collect(Collectors.toSet());
    9. return Objects.equals(text, that.text)
    10. && OrigDatasetFileHelper.duplicateRemoval(set, set2)
    11. && OrigDatasetFileHelper.duplicateRemoval(set2, set);
    12. }
    13. @Override
    14. public int hashCode() {
    15. int result = text == null ? 0 : text.hashCode();
    16. if (CollUtil.isNotEmpty(markedList)) {
    17. //逐个取值累加hashCode
    18. for (TextImpMarkedHypot textImpMarkedHypot : markedList) {
    19. result = 31 * result + textImpMarkedHypot.getText() == null ? 0 : textImpMarkedHypot.getText().hashCode();
    20. }
    21. }
    22. return result;
    23. }
    24. public static boolean duplicateRemoval(Set<String> set1, Set<String> set2) {
    25. for (String s : set1) {
    26. if (!set2.contains(s)) {
    27. return false;
    28. }
    29. }
    30. return true;
    31. }

  • 相关阅读:
    简单了解一下:Node的util工具模块
    linux内核驱动——字符设备实现两个终端单向收发
    地震勘探——相关概念(一)
    从私有Git仓库的搭建到命令的使用再到分支管理,全流程全套服务包您满意
    Axios传值的几种方式
    Spring1
    新手入驻eBay需要哪些条件?eBay防关联同样很重要
    LeetCode 238. 除自身以外数组的乘积(java实现)
    阿里云服务器活动价格及配置整理表(多配置报价)
    java 检查异常与非检查异常
  • 原文地址:https://blog.csdn.net/lililidahaoren/article/details/126159104