当实例被升级时,overridden "Object#equals(Object)" 的结果应该是什么?

What should be the result of overridden "Object#equals(Object)" when instance is upcasted?

我特别关心遵守 Object#equals(Object) 中建立的一般契约的 对称性 部分,其中,给定两个非空对象 xyx.equals(y)y.equals(x)的结果应该是一样的。

假设您有两个 class,PurchaseOrderInternationalPurchaseOrder,其中后者扩展了前者。在基本情况下,对于 x.equals(y)y.equals(x),将每个实例与另一个实例进行比较应该 return 始终如一地 false 是有意义的,因为 PurchaseOrder 是并不总是 InternationalPurchaseOrder,因此 InternationalPurchaseOrder 对象中的附加字段不会出现在 PurchaseOrder.

的实例中

现在,假设您 upcast InternationalPurchaseOrder.

的一个实例
PurchaseOrder order1 = new PurchaseOrder();
PurchaseOrder order2 = new InternationalPurchaseOrder();
System.out.println("Order 1 equals Order 2? " + order1.equals(order2));
System.out.println("Order 2 equals Order 1? " + order2.equals(order1));

我们已经确定结果应该是对称的。但是,如果两个对象包含相同的内部数据,结果应该是 false 吗?我相信结果应该是 true 而不管一个对象有一个额外的字段这一事实。由于通过向上转换 order2,对 InternationalPurchaseOrder class 中的字段的访问受到限制,因此 equals() 方法的结果应为调用 super.equals(obj) 的结果.

如果我说的都是真的,InternationalPurchaseOrderequals方法的实现应该是这样的:

@Override
public boolean equals(Object obj) {
    
     if (!super.equals(obj)) return false;
     
     // PurchaseOrder already passed check by calling super.equals() and this object is upcasted
     InternationalPurchaseOrder other = (InternationalPurchaseOrder)obj;
     
        if (this.country == null) {
            if (other.country != null) return false;
        } else if (!country.equals(other.country)) return false;

        return true;
}

假设country是这个subclass中声明的唯一字段。

问题出在超class

@Override
public boolean equals (Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    PurchaseOrder other = (PurchaseOrder) obj;
    if (description == null) {
        if (other.description != null) return false;
    } else if (!description.equals(other.description)) return false;
    if (orderId == null) {
        if (other.orderId != null) return false;
    } else if (!orderId.equals(other.orderId)) return false;
    if (qty != other.qty) return false;
    return true;
}

因为两个实例不一样class。如果我改用 getClass().isAssignableFrom(Class),对称性就会丢失。使用instanceof时也是如此。在 Effective Java 一书中,Joshua Bloch 间接警告不要不必要地覆盖 equals。然而,在这种情况下,subclass 必须有一个覆盖 equals 的实例来比较在 subclass.

中声明的字段

这已经够久了。这是更复杂的 equals 函数实现的情况,还是只是反对向上转换的情况?

澄清:@Progman 在下面的评论部分提出的建议没有回答这个问题。这不是为 subclasses 覆盖 equals 方法的简单情况。我认为我在此处 post 编写的代码表明我已正确完成此操作。这个 post 特别是关于比较两个对象的预期结果,当一个对象被向上转型时,它的行为就像一个超级 class.

的对象

我认为这里的误解是向上转型实际上改变了对象的类型。

为了开发人员的利益,任何类型的转换都只是指向编译器将变量视为具有特定类型的指针。因此,将类型为 InternationalPurchaseOrder 的变量向上转换为 PurchaseOrder 可能会改变您查看它并与之交互的方式(如您所述,字段受到限制),但该变量仍然引用 InternationalPurchaseOrder

因此要具体回答你的问题,比较类将使用声明为实际类型equals方法,而不是你标记的类型作为.

事实证明,问题比我最初想象的要深一些,主要原因有一个:Liskov 替换原则.

我的PurchaseOrder#equals(Object)方法违反了LSP。因为 InternationalPurchaseOrder 扩展 PurchaseOrder,所以说国际采购 IS-A 采购订单是正确的。因此,为了遵守 LSP,我应该能够将一个实例替换为另一个实例而不会产生任何不良影响。正因为如此,if (getClass() != obj.getClass()) return false;这一行完全违背了这个原则。

即使我的注意力集中在向上转换上(现在我几乎确信这是一种代码味道),几乎同样的问题也适用:我应该能够比较 PurchaseOrderInternationalPurchaseOrder 和 return true 是否在内部包含相同的数据?根据Joshua Bloch book Effective Java, Item 10 (Obey the general contract when override equals),答案应该是yes,但确实不是;但不是出于我最初认为的原因(它们不是同一类型)。问题是这样的,我引用:

There is no way to extend an instantiable class and add value component while preserving the equals contract, unless you're willing to forgo the benefits of object-oriented abstraction.

他继续获取有关此的更多详细信息。结论很简单:这样做会失去抽象的好处,并在此过程中违反 LSP。或者... 只需遵循一个非常重要的编程原则(尤其是在 Java 中):优先组合而不是继承 并注入制作 PurchaseOrder 国内所需的属性或本例中的国际。在我的例子中,这个解决方案看起来像这样(省略了 class 的无关细节):

public class InternationalPurchaseOrder {
    private PurchaseOrder po;
    private String country;

    public (PurchaseOrder po, String country) {
        this.po = po;
        this.country = country;
    }

    public PurchaseOrder asPurchaseOrder() {
        return po;
    }

    @Override
    public boolean equals (Object obj) {
        if (!(obj instanceof InternationalPurchaseOrder)) {
            return false;
        }

        InternationalPurchaseOrder ipo = (InternationalPurchaseOrder)obj;
        return ipo.po.equals(po) && cp.country.equals(country);
    }
}

这样,InternationalPurchaseOrder可以继续与相同 class 的其他实例进行比较 AND 如果需要与 PurchaseOrder 类型,只需要将 asPurchaseOrder 调用到 return 兼容类型的对象即可。