【问题标题】:What happens if we override only hashCode() in a class and use it in a Set?如果我们只覆盖类中的 hashCode() 并在 Set 中使用它会发生什么?
【发布时间】:2014-10-10 14:49:47
【问题描述】:

这可能不是真实世界的场景,只是想知道会发生什么,下面是代码。

我正在创建一组 UsingSet 类的对象。 根据Java中的哈希概念,当我第一次添加包含“a”的对象时,它会创建一个哈希码为97的桶并将对象放入其中。 再次,当它遇到一个带有“a”的对象时,它会调用类 UsingSet 中重写的 hashcode 方法,它会得到 hashcode 97 那么接下来是什么?

由于我没有重写 equals 方法,默认实现将返回 false。那么值“a”的对象将保存在哪个桶中,与之前的哈希码为 97 的对象保存在同一个桶中?还是会创建新的存储桶? 有人知道它将如何在内部存储吗?

/* package whatever; // don't place package name! */

import java.util.*;
import java.lang.*;
import java.io.*;

class UsingSet {  

  String value;  

  public UsingSet(String value){  
    this.value = value;  
  }  

  public String toString() {  
    return value;  
  }  

  public int hashCode() {  
    int hash = value.hashCode();  
    System.out.println("hashcode called" + hash);  
    return hash;  
  }  

  public static void main(String args[]) {  

    java.util.Set s = new java.util.HashSet();  

    s.add(new UsingSet("A"));  
    s.add(new UsingSet("b"));  
    s.add(new UsingSet("a"));  
    s.add(new UsingSet("b"));   
    s.add(new UsingSet("a"));  

    s.add(new Integer(1));  
    s.add(new Integer(1));  

    System.out.println("s = " + s); 

  }  
}  

输出是:

hashcode called65
hashcode called98
hashcode called97
hashcode called98
hashcode called97
s = [1, b, b, A, a, a]

【问题讨论】:

  • 在内部,HashMap 用于存储HashSet 的值。此外,HashMap 下还有一个方法hash(),它应用补充散列函数来防御不良散列码。
  • 应该是同一个bucket,可以在调试器中签入。
  • ans = [A, a, a, 1, b, b]

标签: java collections set


【解决方案1】:

HashCode 和 Equals 方法

  1. 仅覆盖 HashCode,使用默认的 Equals: 只有对同一对象的引用才会返回 true。换句话说,那些你期望相等的对象不会通过调用 equals 方法来相等。
  2. 仅覆盖 Equals,使用默认 HashCode: HashMap 或 HashSet 中可能存在重复项。我们写了equals方法,期望{"abc", "ABC"}等于。但是,在使用 HashMap 时,它们可能会出现在不同的桶中,因此 contains() 方法不会检测到它们。

【讨论】:

    【解决方案2】:

    James Large 的答案不正确,或者说具有误导性(部分也不正确)。我会解释的。

    如果两个对象根据它们的 equals() 方法相等,它们也必须具有相同的哈希码。 如果两个对象具有相同的哈希码,它们也不必相等。

    以下是 java.util.Object 文档中的实际措辞:

    • 如果两个对象根据 equals(Object) 方法相等,则对两个对象中的每一个调用 hashCode 方法必须产生相同的整数结果。
    • 如果根据 equals(java.lang.Object) 方法,如果两个对象不相等,则不要求对两个对象中的每一个调用 hashCode 方法必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

    确实,如果两个对象没有相同的哈希值,那么它们就不相等。但是,散列不是检查相等性的方法 - 因此说它是检查相等性的更快方法是非常不正确的。

    此外,说 hashCode 函数是做任何事情的有效方法也是非常不正确的。这完全取决于实现,但是字符串的 hashCode 的默认实现非常低效,因为字符串变大了。它将根据字符串的每个字符执行计算,因此如果您使用大字符串作为键,那么这将变得非常低效;如果您有大量存储桶,则更是如此。

    在一个 Map 中(HashSet 内部使用了一个 HashMap),有桶,每个桶中有一个链表。 Java 使用 hashCode() 函数来找出它属于哪个桶(它实际上会修改哈希,取决于存在多少桶)。由于两个对象可能共享相同的哈希,接下来会依次遍历链表,检查 equals() 方法以查看对象是否重复。根据 java.util.Set 文档:

    一个不包含重复元素的集合。

    因此,如果它的 hashCode() 将其引导到一个存储桶,其中该存储桶包含一个 Object,其中 .equals() 的计算结果为 true,那么之前的 Object 将被新的 Object 覆盖。您可能可以在此处查看更多信息: How does a Java HashMap handle different objects with the same hash code?

    一般来说,如果你覆盖了 hashCode 函数,你也覆盖了 equals 函数(如果我没记错的话,如果你选择不这样做,这会违反合同),这是一个很好的做法。

    【讨论】:

    • 您已经提到“如果它的 hashCode() 将它引导到一个存储桶,其中该存储桶包含一个 .equals() 评估为真的对象,那么以前的对象将被新的对象覆盖目的”。但我想知道,“如果它的 hashCode() 将它引导到一个存储桶,其中该存储桶包含一个 .equals() 评估为 (false) 的对象”会发生什么。我想知道这两个对象在内部如何存储在同一个桶中?
    • 正如我所说,每个桶都是一个链表。 hashCode 根据桶的数量被截断,以使其更有效地结合存储的元素数量。这意味着存储的元素数量越少,您就越有可能收到碰撞。当映射找到属于哈希的存储桶时,它将遍历链表中的每个元素。如果它到达链表的末尾并且没有匹配的元素,它将把新元素放在末尾。如果地图达到其负载因子,则重构整个地图并增加
    • 桶数。这有点离题,但负载因子默认为 75%。此外,再次偏离主题,这就是为什么您要设置一个初始容量,该容量反映在您认为地图中的总元素数量上,因为随着存储的元素数量越来越大,重构地图的成本越来越高和更大。您希望将初始容量设置为刚好超过您认为将要存储的最大元素数,以在以后降低此成本。
    • Re,“散列不是检查相等性的方法......”:您可以通过在调用 .equals() 之前比较散列码来节省大量时间。这正是 HashMap.get(k) 在 OpenJDK 7 中所做的(查找源代码。)它总是先比较哈希码,如果给定键的哈希码和表中的键不相等,那么它将不必费心调用 .equals() 方法。这节省了大量时间,特别是对于只计算一次自己的哈希码,然后在后续的 .hashCode() 调用中返回相同值的不可变对象(例如字符串)。
    • 关于“字符串的 hashCode 的默认实现非常低效”,第一次调用 String.hashCode() 会根据字符串的每个字符计算哈希码。然后它保存它。对 s.hashCode() 的每次后续调用都只返回保存的值。对于用作 HashMap 键的 String,可能会多次调用 hashCode() 方法。当然,other 字符串可能每次都是一个新字符串,但是计算一个字符串的哈希码(即从中获取字节)仍然比从两个字符串中获取字节以便比较便宜他们。
    【解决方案3】:

    Set 的行为会有所不同。

    独特性不会发生。因为唯一性将通过 hashcode 和 equals 方法来实现。
    输出会喜欢这个 s = [A, a, b, 1] 而不是早期的。

    除了删除和包含所有都行不通。

    【讨论】:

      【解决方案4】:

      不看你的代码...

      哈希码的全部意义在于加快测试两个对象是否相等的过程。测试两个大型复杂对象是否相等的成本可能很高,但比较它们的哈希码非常容易,而且哈希码可以预先计算。

      规则是:如果两个对象没有相同的哈希码,那意味着他们不相等。无需进行昂贵的平等测试。

      所以,你标题中问题的答案:如果你定义一个 equals() 方法说对象 A 等于对象 B,并且你定义一个 hashCode() 方法说对象 A 是 not 等于对象 B(即,它表示它们具有不同的哈希码),然后您将这两个对象交给某个库,该库关心它们是否相等(例如,如果您将它们放在哈希表中),那么库的行为将是未定义的(即,可能错误)。


      添加信息:哇!我真的很怀念这里只见树木不见森林——思考 hashCode() 的目的而不将其置于 HashMap 的上下文中。如果 m 是一个包含 N 个条目的 Map,并且 k 是一个键;打电话m.get(k)的目的是什么?显然,目的是在映射中搜索其键 等于 到 k 的条目。

      如果没有发明散列码和散列映射会怎样?好吧,假设键具有自然的总顺序,您可以做的最好的事情是搜索 TreeMap,将给定的键与 O(log(N)) 其他键的相等性进行比较。在最坏的情况下,键没有顺序,您必须将给定的键与映射中的每个键进行比较,直到找到匹配项或全部测试。换句话说,m.get(k) 的复杂度是 O(N)。

      当m为HashMap时,m.get(k)的复杂度为O(1),key是否可以排序。

      所以,我说散列码的目的是加快测试 两个 对象是否相等的过程,这搞砸了。它实际上是关于测试一个对象与整个 collection 其他对象的相等性。这就是比较哈希码不仅仅有一点帮助的地方。它有几个数量级的帮助......

      ...如果 k.hashCode()k.equals(o) 方法遵守规则:j.hashCode()!=k.hashCode() 暗示 !j.equals(k)

      【讨论】:

      • 我建议修改您的答案或将其删除。你说的大部分都是错的,如果不是你说的全部的话。编辑:是的,你说的都是错的。
      • @searchengine27,好的,所以您是说比较大型复杂对象的相等性永远不会昂贵,比较哈希码总是很困难,而且它永远不可能/对对象有用计算一次后缓存其哈希码。您是说两个具有不同哈希码的对象测试为相等是可以的,并且如果您将这样的对象放入哈希表中,哈希表将对它们做一些可预测和有用的事情。或者,我想,也许,“你说的都是错的”可能有一些我还没有听说过的新含义。
      • 阅读我发布的答案。我想解决您在我的回答中提到的一些 事情,但我无意中在我的回答中解决了关于您的回答的所有错误。总结是:您需要阅读文档。至于比较字符串的相等性,它将比较字符串中的每个字符,这是每个字符的单个操作。 hashCode 将执行“h = 31*h + val[off++];” (这是 5 次操作)在每个字符上。这意味着,hashCode 总是比 equals 贵 5 倍。无论如何,这无关紧要,因为文档没有说相等的 hashCodes 意味着相等。
      • 我刚刚在 OpenJDK 7 中查找了 String.hashCode() 的源代码。第一次调用它时,它会按照你说的做,但随后它也会将哈希值保存在实例变量中.在随后的每次调用中,它只返回保存的值。琐碎的。哦,我并没有说相等的 hashCodes 应该暗示平等:我说 unequal 哈希码应该暗示 non 平等。
      • @searchengine27 重新“5 次操作”。您计算的方式, String.equals() 不会对每个字符执行 one 操作。它做了五个:它从每个数组中获取一个字符,增加两个索引,然后比较字符。但是“操作”并不是正确的计算方法:获取的成本比加法、增量和乘法的成本要高得多。您应该指出的是s.hashCode() 必须检查整个字符串,但s.equals(t) 一发现差异就会停止(例如,它将获取如果第一个字符不同,则每个字符串中只有一个字符)
      【解决方案5】:

      您可以简单地将 hashcode 和 equals 方法假设为 2D 搜索,例如:-

      其中 Hashcode 是 Rows,对象列表是 Column。 考虑以下类结构。

      public class obj
        {
          int Id;
          String name;
          public obj(String name,int id)
           {
               this.id=id;
               this.name=name;
           }
        }
      

      现在如果你像这样创建对象:-

      obj obj1=new obj("Hassu",1);
      obj obj2=new obj("Hoor",2);
      obj obj3=new obj("Heniel",3);
      obj obj4=new obj("Hameed",4);
      obj obj5=new obj("Hassu",1);
      

      然后您将这些对象放置在地图中,如下所示:-

          HashMap hMap=new HashMap();
         1. hMap.put(obj1,"value1");
         2. hMap.put(obj2,"value2");
         3. hMap.put(obj3,"value3");
         4. hMap.put(obj4,"value4");
         5. hMap.put(obj5,"value5");
      

      现在,如果您没有覆盖哈希码和等于,那么在将所有对象放置到第 5 行之后,如果您将 obj5 作为默认哈希码放在地图中,您将获得不同的哈希码,因此行(存储桶将不同)。 所以在运行时内存中会这样存储。

      |hashcode   | Objects   
      |-----------| --------- 
      |000562     | obj1 
      |000552     | obj2 
      |000588     | obj3
      |000546     | obj4
      |000501     | obj5
      

      现在,如果您创建相同的对象,例如:- obj obj6 = new obj("hassu",1); 如果你在 map.like 中搜索这个值

      if(hMap.conaints(obj6)) 
      or 
      hMpa.get(obj 6);
      

      虽然具有相同内容的 key(obj1) 可用,但您将分别得到 false 和 null。 现在,如果您仅覆盖 equals 方法。 并执行相同的内容搜索键也将获得 Null,因为 obj6 的 HashCode 不同,并且在该哈希码中您找不到任何键。 现在如果你只覆盖 hashCode 方法。

      您将获得相同的存储桶(HashCode 行),但无法检查内容,它将采用 Super Object Class 的引用检查实现。 所以在这里如果你搜索键 hMap.get(obj6) 你会得到正确的哈希码:- 000562 但由于 obj1 和 obj6 的引用不同,你会得到 null。

      【讨论】:

        猜你喜欢
        • 2015-01-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-01
        • 2014-04-19
        相关资源
        最近更新 更多