当实例向上转换时,重写“Object#equals(Object)”的结果应该是什么?

问题描述

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

假设您有两个类,PurchaSEOrderInternationalPurchaSEOrder,其中后者扩展了前者。在基本情况下,将每个实例与另一个实例进行比较应该为 falsex.equals(y) 返回一致的 y.equals(x) 是有道理的,因为 PurchaSEOrder 并不总是 { {1}} 因此,InternationalPurchaSEOrder 对象中的附加字段不会出现在 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) 时也是如此。在《Effective Java》一书中,Joshua Bloch 间接警告了不必要的覆盖 instanceof。但是,在这种情况下,子类必须具有覆盖的 equals 实例以比较子类中声明的字段。

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

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

解决方法

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

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

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

,

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

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

即使我的重点是向上转换(现在我非常确信这是一种代码异味),几乎同样的问题也适用:我是否应该能够比较 PurchaseOrder 和 {{1 }} 并返回 InternationalPurchaseOrder 如果它们在内部包含相同的数据?根据 Joshua Bloch 的书Effective Java,第 10 条(覆盖 equals 时遵守一般约定),答案应该是肯定的,但确实不是;但不是因为我最初认为的原因(它们不是同一种类型)。问题是这样的,我引用:

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

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

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 可以继续与同一类的其他实例进行比较AND,所有这些都是需要的是调用 PurchaseOrder 以返回兼容类型的对象。