在Java中覆盖equals和hashCode时应该考虑哪些问题?

问题

重写equalshashCode时必须考虑哪些问题/陷阱?


#1 热门回答(1365 赞)

###理论(语言律师和数学倾向):

 equals()(javadoc)必须定义一个等价关系(它必须是自反的,对称的和反式的)。另外,它必须是一致的(如果没有修改对象,那么它必须保持返回相同的值)。此外,o.equals(null)必须始终返回false。

 hashCode()(javadoc)也必须是一致的(如果对象没有按照equals()进行修改,它必须保持返回相同的值)。

两种方法之间的关系**是:

每当a.equals(b),则a.hashCode()必须与b.hashCode()相同。

在实践中:

如果你覆盖一个,那么你应该覆盖另一个。

使用用于计算equals()的同一组字段来计算hashCode()

使用来自theApache Commons Langlibrary的优秀助手类EqualsBuilderHashCodeBuilder。一个例子:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

###还要记住:

当使用基于散列的CollectionMap,如HashSet,LinkedHashSet,HashMap,HashtableWeakHashMap时,请确保放入集合的关键对象的hashCode()在对象位于集合中时永远不会更改。确保这一点的防弹方法是使你的密钥不可变,which has also other benefits


#2 热门回答(280 赞)

如果你正在处理使用像Hibernate这样的对象关系映射器(ORM)持久化的类,那么有些问题值得注意,如果你认为这已经不合理地复杂了!
延迟加载的对象是子类
如果使用ORM持久化对象,则在许多情况下,你将处理动态代理以避免从数据存储中过早加载对象。这些代理实现为你自己的类的子类。这意味着this.getClass()== o.getClass()将返回false。例如:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

如果你正在处理ORM,使用o instanceof Person是唯一能正常运行的东西.延迟加载的对象具有空字段
ORM通常使用getter来强制加载延迟加载的对象。这意味着如果person是延迟加载的,person.name将是null,即使person.getName()强制加载并返回“John Doe”。根据我的经验,这更常出现在hashCode()equals()中。
如果你正在处理ORM,请确保始终使用getter,并且永远不要在hashCode()equals()中进行字段引用.保存对象将改变其状态
持久对象通常使用id字段来保存对象的键。首次保存对象时,此字段将自动更新。不要在hashCode()中使用id字段。但你可以在equals()中使用它。

我经常使用的模式是

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

但是:你不能在hashCode()中包含getId()。如果你这样做,当一个对象被持久化时,它的hashCode就会改变。如果对象在HashSet中,你将“永远不会”再次找到它。

在我的Person例子中,我可能会使用getName()来表示hashCodegetId()加上getName()(仅用于偏执)``equals()。如果存在“hashCode()”的“冲突”风险,那就没关系,但是对于equals()来说永远都不行。  hashCode()应该使用equals()`中不变的属性子集


#3 热门回答(78 赞)

关于obj.getClass()!= getClass()的澄清。

这个语句是equals()继承不友好的结果。 JLS(Java语言规范)规定如果A.equals(B)== trueB.equals(A)也必须返回true。如果省略该语句继承覆盖equals()(并更改其行为)的类将破坏此规范。

请考虑以下示例,省略语句时会发生什么:

class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }

new A(1).equals(new A(1))另外,new B(1,1).equals(new B(1,1))结果给出true,应该如此。

这看起来非常好,但看看如果我们尝试使用这两个类会发生什么:

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

显然,这是错误的。

如果要确保对称条件。 a = b如果b = a且Liskov替换原则不仅在B实例的情况下调用super.equals(other),而且在'A`实例之后检查:

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

哪个会输出:

a.equals(b) == true;
b.equals(a) == true;

其中,如果a不是'B的引用,那么它可能是类'A的引用(因为你扩展它),在这种情况下你也调用super.equals()****。