当实例被升级时,overridden "Object#equals(Object)" 的结果应该是什么?
What should be the result of overridden "Object#equals(Object)" when instance is upcasted?
我特别关心遵守 Object#equals(Object)
中建立的一般契约的 对称性 部分,其中,给定两个非空对象 x
和y
,x.equals(y)
和y.equals(x)
的结果应该是一样的。
假设您有两个 class,PurchaseOrder
和 InternationalPurchaseOrder
,其中后者扩展了前者。在基本情况下,对于 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)
的结果.
如果我说的都是真的,InternationalPurchaseOrder
中equals
方法的实现应该是这样的:
@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;
这一行完全违背了这个原则。
即使我的注意力集中在向上转换上(现在我几乎确信这是一种代码味道),几乎同样的问题也适用:我应该能够比较 PurchaseOrder
和 InternationalPurchaseOrder
和 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 兼容类型的对象即可。
我特别关心遵守 Object#equals(Object)
中建立的一般契约的 对称性 部分,其中,给定两个非空对象 x
和y
,x.equals(y)
和y.equals(x)
的结果应该是一样的。
假设您有两个 class,PurchaseOrder
和 InternationalPurchaseOrder
,其中后者扩展了前者。在基本情况下,对于 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)
的结果.
如果我说的都是真的,InternationalPurchaseOrder
中equals
方法的实现应该是这样的:
@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;
这一行完全违背了这个原则。
即使我的注意力集中在向上转换上(现在我几乎确信这是一种代码味道),几乎同样的问题也适用:我应该能够比较 PurchaseOrder
和 InternationalPurchaseOrder
和 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 兼容类型的对象即可。