有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java是否应该hashCode()只使用equals()中使用的不可变字段的子集?

形势

我需要重写equals(),并且按照建议,我还使用相同的字段重写了hashCode()方法。然后,当我看到一个集合时,它只包含一个对象,我得到了令人沮丧的结果

set.contains(object)
=> false

set.stream().findFirst().get().equals(object)
=> true

我现在了解到,这是由于object在被添加到set之后对object进行了更改,而set再次更改了其哈希代码contains然后查看错误的键,找不到object

我对实施的要求如下:

  • 需要可变字段来正确实现equals()
  • 在基于散列的CollectionsMaps例如ashHashSet中安全地使用这些对象,即使它们易于更改

哪一个与公约相冲突

问题

仅使用equals()中使用的字段子集来计算hashCode()而不是使用all,是否存在任何危险

更具体地说,这意味着:equals()使用对象的许多字段,而hashCode()只使用那些在equals()中使用且不可变的字段

我想这应该没问题,因为

  • contract是完全填充的:相等的对象将生成相同的哈希代码,而相同的哈希代码并不一定意味着对象是相同的
  • 对象的哈希代码保持不变,即使对象暴露于更改中,因此在更改前后的HashSet中也会找到该对象

帮助我理解我的问题而不是解决问题的相关帖子:What issues should be considered when overriding equals and hashCode in Java?Different fields for equals and hashcode


共 (4) 个答案

  1. # 1 楼答案

    合同确实会履行。契约规定.equal()对象总是相同的.hashCode()。相反的情况并不一定是真的,我想知道一些人和IDE是否痴迷于应用这种做法。如果这对所有可能的组合都是可能的,那么您将发现完美的哈希函数

    顺便说一句,IntelliJ在生成hashCode和equals时提供了一个很好的向导,分别处理这两个方法,并允许区分您的选择。显然,相反,在hashCode()中提供更多字段,在equals()中提供更少字段,将违反合同

  2. # 2 楼答案

    hashCode()可以使用equals()使用的字段的子集,尽管这可能会使性能略有下降

    您的问题似乎是由于在对象仍在集合中时,以改变hashCode()和/或equals()功能的方式修改对象引起的。无论何时将对象添加到HashSet(或作为HashMap中的键),都不能随后修改equals()和/或hashCode()使用的该对象的任何字段。理想情况下,equals()使用的所有字段都应该是final。如果不能,则当对象在集合中时,必须将其视为最终对象

    TreeSet/TreeMap也是如此,但适用于compareTo()使用的字段

    如果确实需要修改equals()(或者在树集/树映射的情况下,compareTo())使用的字段,则必须:

    1. 首先,从集合中移除该对象
    2. 然后修改对象
    3. 最后再把它添加到场景中
  3. # 3 楼答案

    对于HashSet和类似的集合/映射,让hashCode()只使用equals()方法中的字段子集是一个有效的解决方案。当然,您必须考虑哈希代码对于减少映射中的冲突有多有用

    但是要注意,如果你想使用像TreeSet这样的有序集合,问题会再次出现。然后,您需要一个比较器,该比较器从不为“不同”对象提供冲突(返回零),这意味着集合只能包含一个冲突元素。您的equals()描述意味着将存在多个仅在可变字段中不同的对象,然后您将丢失:

    • 在compareTo()方法中包含可变字段可以更改比较符号,因此对象需要移动到树中的其他分支
    • 排除compareTo()方法中的可变字段会限制树集中最多有一个冲突元素

    因此,我强烈建议您重新考虑一下对象类的平等性和易变性概念

  4. # 4 楼答案

    这对我来说完全正确。假设你有一个Person

     final int name; // used in hashcode
     int income; // name + income used in equals
    

    name决定条目的去向(想想HashMap)或选择哪个bucket

    你把一个Person作为Key放在HashMap里面:根据hashcode,它会进入某个桶,例如第二个桶。升级income并在地图中搜索该Person。根据hashcode它必须在第二个桶中,但根据equals它不在那里:

     static class Person {
        private final String name;
    
        private int income;
    
        public Person(String name) {
            super();
            this.name = name;
        }
    
        public int getIncome() {
            return income;
        }
    
        public void setIncome(int income) {
            this.income = income;
        }
    
        public String getName() {
            return name;
        }
    
        @Override
        public int hashCode() {
            return name.hashCode();
        }
    
        @Override
        public boolean equals(Object other) {
            Person right = (Person) other;
    
            return getIncome() == right.getIncome() && getName().equals(right.getName());
        }
    
    }
    

    还有一个测试:

        HashSet<Person> set = new HashSet<>();
        Person bob = new Person("bob");
        bob.setIncome(100);
        set.add(bob);
    
        Person sameBob = new Person("bob");
        sameBob.setIncome(200);
    
        System.out.println(set.contains(sameBob)); // false
    

    我认为你缺少的是一个事实,即hashcode决定一个条目所在的存储桶(该存储桶中可能有许多条目),这是第一步,但equals决定这是否是一个平等的条目

    你提供的例子是完全合法的;但是你链接的那一个是另一种方式——它在hashcode中使用了更多字段,因此不正确

    如果您了解这些细节,首先使用hashcode来了解条目可能位于哪里,然后才尝试通过^{找到所有这些条目(来自子集或bucket),那么您的示例就有意义了