重写equals方法

3

这里有一个初学者的问题:

在我的大学作业中,我需要为我创建的新类重写对象类的equals方法。

新类是“Product”,每个产品都有一个唯一的“id”属性。以下是我如何重写它:

@Override
public boolean equals(Object obj) {
       final Product other = (Product) obj;
       if (id != other.id)
           return false;
       return true;
   }

问题在于这只是10分中的1.5分,这让我怀疑它是否如此简单。因此,我开始搜索并发现了以下内容:

@Override
public boolean equals(Object obj) {
       if (this == obj)
           return true;
       if (obj == null)
           return false;
       if (getClass() != obj.getClass())
           return false;
       final Product other = (Product) obj;
       if (id != other.id)
           return false;
       return true;
   }

我觉得这些都没有意义,因为我认为最后一个 if 语句会检查所有其他 if 语句的限制。你们觉得呢?哪种方式更好地覆盖这个方法?

谢谢!


1
等式的契约规定所有相等的对象必须有相同的哈希码。因此,如果你重写equals()方法,就必须重写hashCode()方法。 - Michael Krussel
5个回答

14

第二段代码更好:

  • 它优化了x.equals(x),虽然这不是必要的正确性,但这是一项有用的优化。
  • 它可以处理x.equals(null)而不是抛出NullPointerException异常。
  • 它可以处理完全不同类的对象,而不会像你的代码那样抛出ClassCastException异常(例如:x.equals("foo"))。
  • 它要求提供的类型完全相同以提供对称关系;否则,obj.equals(x)可能会调用另一个方法,从而得出不同的结果。

如果您在不更改equals()方法的语义的情况下子类化Product,则第二段代码将无法工作。 - ignis
2
@ignis:这取决于你所说的“工作”是什么意思。它不会将Product的实例视为子类的实例相等,但我认为这并不是错误,只是需要记录下来的行为。 - Jon Skeet
如果a.getClass() == Product.class,b是Product的某个子类实例,并且a和b具有相同的内部状态,则a.equals(b)将始终返回false。因此,equals()关系不是对称的。这违反了java.lang.Object的契约。 - ignis
@ignis:只有当b.equals(a)返回true时才会是非对称的......如果它调用了super.equals(a),它就不会返回true。没有这个检查,a.equals(b)将只检查id,因此它可能返回true,而b.equals(a)将返回false,因为a不是与Product相同子类的实例或该子类的子类。调用getClass()的目的恰好是为了帮助对称性。 - Jon Skeet
@ignis:如果a和b是不同的类,它们怎么能代表同一件事呢?为什么你的例子不对称?a.equals(b)会返回false(因为a.class == Product.class而b.class = SubProduct.class),而b.equals(a)也会返回false(原因相同)。如果他使用instanceof,那么equals就会产生不对称的结果。 - nojo
1
@nojo:如果有一个抽象类代表了一组不可变的值的组合(例如,一个由double组成的二维矩阵),但不强制要求特定的存储格式,那么根据这些值定义相等性和哈希码可能是有意义的。在这种情况下,一个Size为三的IdentityMatrix可能与一个DiagonalValues为{1.0,1.0,1.0}的DiagonalMatrix相等。 - supercat

2
第二个版本是一个安全的版本,我会说是一个过于严谨的版本。相反,你的版本可能会引发ClassCastException异常,因为你假设变量obj的运行时类型是product类型。这是不正确的,所以你应该使用this.getClass() != obj.getClass()(你也可以使用instanceof运算符来解决这个问题)。如果我执行
Product p = new Product();
p.equals("abc");

当我应该得到false时,我收到了一个异常。

此外,它还解决了product.equals(null)的问题,这应该返回false,如文档中的equals合同方法所述。如果您不关心这个问题,并且在您的equals方法中处理:

...
Product p = (Product)obj; // obj is null
obj.id // this throws a NullPointerException

1
如果在一个非final类中使用instanceof,当这个类被子类化时会产生对称性问题。 - Jon Skeet

1
在覆盖 equals() 方法中常用的习语是:
@Override
public boolean equals(Object obj) {
       if (! (obj instanceof Product) ) return false;

       final Product other = (Product) obj;
       if (id != other.id)
           return false;
       return true;
   }

在您发布的第二个版本中:
  • 如果以下检查过于昂贵,则第一个if()可能仅用于优化。但这并不是情况,因此这只是重复的代码,是有害的。
  • 如果定义了一个Product子类,其不改变方法equals()的语义(例如提供一些便利方法但没有为对象提供额外的内部状态),那么该版本将无法工作。这是由第三个if()引起的。

0
“Joshua Bloch:Effective Java”提出的解决方案是(假设Product没有除Object以外的超类):
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Product)) return false;
    final Product other = (Product) obj;
    if (id != other.id) return false;
    return true;
}

你的第一个解决方案有两个缺点:

  • new Product(1).equals(null) 抛出 NullpointerException,尽管在 Object.equals() 中指定返回 false。
  • new Product(1).equals(new Vector()) 抛出 ClassCastException,尽管在 Object.equals() 中指定返回 false。

这两个问题都可以通过实例检查来解决。if (this == obj) return true; 对于效率通常很有用,但在这里可能不是必要的。

你发布的第二个解决方案使得编写具有良好语义的 Product 子类变得困难。如果你有一个子类

public class SubProduct extends Product {
    SubProduct(int id) {
         super(id);
    }
    ...
}

你会得到!new Product(4).equals(new SubProduct(4))。这违反了Liskov替换原则,通常被认为不太好。如果你有一个final类,第二个解决方案与上面的解决方案相同。


考虑如果 SubProduct 有更多的状态,并且进一步覆盖了 equals - 那么如果使用 instanceof,你最终得到的是 a.equals(b)!= b.equals(a),这违反了 Object 契约。在 Liskov 方面,使用精确类并不总是您想要的,但它允许子类化而不会破坏 Object 契约。在继承层次结构中进行相等性比较基本上是很麻烦的 :( - Jon Skeet
完全同意。我特别避免说“错误”。然而,在阅读其他答案时,我没有那么仔细,错过了你更简洁的反例。 - Christoph Zenger

0

在最安全的覆盖equals方法的方式中,第2种方法来自于《Effective Java》。如果Object为空,则第1种方法会出现空指针,并且它不够优化(没有检查ojb是否是对自身的引用)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接