除了不可变值对象之外,何时应该重写 `equals()` 方法?

4
很明显,当你处理不可变的值对象(如map键、需要在包含它们的对象之间进行比较的强类型字段值等)时,equals()(当然还有hashCode())是很有价值的。
但除了值对象之外,你有多少可能会拥有两个独立构造的实例,并希望它们相等的情况呢?
对我来说很难想象出一个现实场景,在这种情况下引用相等性并不能得到你想要的结果;而在那些特定情况下,似乎一个特定于场景的等价方法(例如isEquivalentTo(Foo)而不是equals(Object))会更加安全,特别是对于可变对象而言。
在非值类型中使用equals()的用例是什么?

2
有时我会在基类中覆盖它们并将它们设置为final,以确保该类型的所有类具有与Object.equals()Object.hashCode()相同的语义。那不是你所问的 :-) - Raedwald
@Raedwald 所以你的意思是把它们设为final,以确保其他人不会在以后覆盖Object的默认行为? :) - David Moles
我支持这种防御性编程。 :) - David Moles
很明显可能并没有一个(至少没有真正重要的)。但是,你将如何重新设计Java的核心对象层次结构,以反映对于某些对象来说equals()应该是final,而对于补充类别则可以被重写? - FoolishSeth
好问题。虽然我不是一名语言设计师,但我的第一反应是认为解决方案与将不可变性作为顶层语言概念有关,并分离身份和状态的概念(例如Clojure所做的那样)。 - David Moles
2个回答

2

嗯,Set<E>是可变的,并且有一个(有用的)equals()定义。这似乎并不无用...


1
集合的相等方法有点有用。我时不时地使用它们——通常是在单元测试断言中懒惰的时候。但并不经常。它们不像containsAll()这样的东西那么有用,而且我不明白为什么它们是个好主意。 - David Moles
1
首先,当集合是不可变的时候——通过(谨慎地)使用Collections.unmodifiableSet或者Guava的ImmutableSet等方式——你可以将它们用作映射中的键,或者其他任何你喜欢的方式。虽然大多数情况下,对于可变类型来说,拥有一个equals()方法并没有什么特别有用的地方,但是...如果你需要测试等价性,那么,为什么不重写equals而不是创建自己的isEquivalentTo方法呢? - Louis Wasserman
对于一个不可变类来说,覆盖equals()没有任何问题(我可以看出在不可变集合框架中,例如子列表或子树共享中,它可能非常有用)。对于可变类,isEquivalentTo()不会让您遇到其他类或标准库(例如集合框架)所假定的 equals()hashCode() 不稳定的任何问题。 - David Moles
那显然是一个决定,允许例如 Set 作为映射键使用的实用性超过了风险。我认为这是正确的决定。例如技术上可变的映射键的使用是如此罕见,以至于它不构成主要风险。 - Louis Wasserman

0

来自docs.oracle.com

Interface List<E>

boolean contains(Object o)

Returns true if this list contains the specified element. 

More formally, returns true if and only if this list contains at least one element e such that (o==null ? e==null : o.equals(e)).

因此,覆盖 equals 对于这种(和其他)语言结构非常有用。

考虑以下代码(抱歉,它是用 C# 写的):

class MimeType
{
    public string Name { get; set; }
    public string Extension { get; set; }    

    public override bool Equals(object obj)
    {
        var other = obj as MimeType;
        if (other == null) return false;
        return other.Name == Name;
    }
}

class Program
{
    void Main()
    {
        var jpeg1 = new MimeType();
        jpeg1.Name = "image/jpeg";
        jpeg1.Extension = ".jpg";

        var jpeg2 = new MimeType();
        jpeg2.Name = "image/jpeg";
        jpeg2.Extension = ".jpeg";    

        var list = new List<MimeType>();
        list.Add(jpeg1);

        if (!list.Contains(jpeg2))
            list.Add(jpeg2);
    }
}

在上面的例子中,jpeg2 对象不会被添加到列表中,因为该列表已经包含一个等效的实例。
更新:
只是为了好玩,我添加了 MimeType 类的 Extension 成员。

2
是的,那将是值类型的一个例子。我真的希望它是不可变的! :) - David Moles
我更新了我的小例子。添加扩展名是否会使MimeType类成为可变类型?这并不意味着它们仍然等价,但我可以想象一些情况下仍然希望将两个实例视为相等。 - ken
这就是我想问的:你能想象出哪些情况? - David Moles

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