-1

我特别关注遵守在 where 中建立的一般合同的对称部分Object#equals(Object),给定两个非空对象xand , andy的结果应该是相同的。x.equals(y)y.equals(x)

假设您有两个类,PurchaseOrder并且InternationalPurchaseOrder后者扩展了前者。false在基本情况下,将每个实例与另一个实例进行比较应该一致地返回是有意义的,x.equals(y)并且y.equals(x)仅仅因为 aPurchaseOrder并不总是 anInternationalPurchaseOrder因此,InternationalPurchaseOrder对象中的附加字段不会出现在 的实例中PurchaseOrder

现在,假设您向上转换了一个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受到限制,因此equals()方法的结果应该是对super.equals(obj).

如果我所说的都是真的,那么equals方法的实现InternationalPurchaseOrder应该是这样的:

@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是该子类中声明的唯一字段。

问题出在超类

@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;
}

因为这两个实例不属于同一类。如果我改为使用getClass().isAssignableFrom(Class),则失去对称性。使用时也是如此instanceof。在《Effective Java》一书中,Joshua Bloch 间接警告了equals不必要的覆盖。然而,在这种情况下,子类必须有一个覆盖equals实例以比较子类中声明的字段。

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

澄清:@Progman 在下面的评论部分提出的建议没有回答这个问题。这不是覆盖equals子类方法的简单案例。我认为我在这里发布的代码表明我已经正确地做到了这一点。这篇文章特别是关于比较两个对象的预期结果,当一个对象被向上转换时,它的行为就像超类的对象一样。

4

2 回答 2

1

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

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

即使我的重点是向上转换(现在我几乎确信这是一种代码异味),几乎同样的问题也适用:如果它们在内部包含相同的数据,我是否应该能够比较 and 的实例PurchaseOrderInternationalPurchaseOrder返回true?根据 Joshua Bloch book Effective Java , Item 10 (Obey the general contract when overriding equals),答案应该是肯定的,但确实不是;但不是因为我最初认为的原因(它们不是同一类型)。问题是这样的,我引用:

equals除非您愿意放弃面向对象抽象的好处,否则无法在保留契约的同时扩展可实例化类并添加价值组件。

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

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可以继续将 an 与同一类的其他实例进行比较,并且如果需要与PurchaseOrder类型的对象进行比较,只需调用asPurchaseOrder即可返回兼容类型的对象。

于 2021-03-08T05:43:10.300 回答
1

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

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

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

于 2021-03-07T20:57:48.060 回答