一个实例可以等于某个更特定类型的其他实例吗?


25

我读过这篇文章:如何用Java编写平等方法

基本上,它为支持继承的equals()方法提供了一种解决方案:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

但这是个好主意吗?这两个实例看起来是相等的,但可能具有两个不同的哈希码。是不是有点不对劲?

我相信这可以通过改用操作数来更好地实现。


1
链接中给出的带有彩色点的示例对我来说更有意义。我认为2D点(x,y)可以看作是Z分量为零(x,y,0)的3D点,并且我希望等式在您的情况下返回false。实际上,在本文中,明确指出ColoredPoint与Point有所不同,并且始终返回false。
coredump

10
没有什么比打破常规的教程更糟的了……要从程序员那里摆脱这种习惯需要花费数年的时间。
corsiKa

3
@coredump将2D点视为零z坐标对于某些应用程序可能是一个有用的约定(考虑到处理早期数据的早期CAD系统)。但这是一个任意约定。具有3个或更多维度的空间中的平面可以具有任意方向...这就是使有趣的问题变得有趣的原因。
本·鲁格斯2015年

Answers:


71

这不应该是平等的,因为它破坏了传递性。考虑以下两个表达式:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

由于相等是可传递的,因此这应意味着以下表达式也适用:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

但是,当然-不是。

因此,您的转换想法是正确的-期望在Java中,转换只是意味着转换引用的类型。您真正想要的是一种转换方法,可以Point2D从一个Point3D对象创建一个新对象。这也将使表达式更有意义:

twoD.equals(threeD.projectXY())

1
本文介绍了破坏传递性的实现,并提供了一系列变通办法。在我们允许2D点的域中,我们已经决定第三维不重要。所以(10, 20, 50)等于 (10, 20, 60)就好。我们只关心1020
本·鲁格斯2015年

1
是否应该Point2D有一种projectXYZ()方法来Point3D表示自己?换句话说,实现应该彼此了解吗?
hjk

4
@hjk消除Point2D似乎更简单,因为投影2D点需要首先在3D空间中定义其平面。如果2D点知道它是平面,则它已经是3D点。如果没有,则无法投影。我想起了雅培的Flatland
本·鲁格斯2015年

@benrudgers不过,您可以定义一个Plane3D对象,该对象将在3D空间中定义一个平面,该平面可以有一个lift方法(2D-> 3D正在举起,而不是投影),该方法将接受a Point2D和一个数字作为“第三轴” “-沿平面法线到平面的距离。为了易于使用,您可以将公共平面定义为静态常量,因此您可以执行以下操作Plane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50))
Idan Arye

@IdanArye我在评论2D点应该有投影方法的建议。对于具有升力方法的飞机,我认为这需要两个参数才有意义:一个2D点和假定位于其上的飞机,即,如果它不拥有该点,则它确实需要是一个投影...如果它拥有该点,为什么不仅仅拥有一个3D点,并消除有问题的数据类型和一种笨拙的方法的味道呢?YMMV。
本·鲁格斯2015年

10

我没有阅读有关Alan J. Perlis 的智慧的文章:

Epigram 9.在一个数据结构上运行100个函数比在10个数据结构上运行10个函数要好。

正确地实现“平等”是使Scala的发明者Martin Ordersky晚上起来的那种问题,这应该让人们暂停一下是否equals在继承树中重写是否是一个好主意。

当我们不幸获得a时,发生的事情ColoredPoint是我们的几何图形失败了,因为我们使用继承来扩散数据类型,而不是制造一个好的类型。尽管必须返回并修改继承树的根节点才能equals正常工作。为什么不加zcolorPoint

这样做有充分的理由,Point并且ColoredPoint可以在不同的域中进行操作……至少如果这些域从未相互混合过。但是,如果是这种情况,我们就不必重写equals。比较ColoredPointPoint平等仅在允许他们混合的第三个领域才有意义。在那种情况下,最好为第三个域量身定制“平等”,而不是尝试从一个或另一个或两个非混杂域中应用平等语义。换句话说,“平等”应定义为我们从两侧流入泥浆的地方,因为即使六个月前凌晨2点ColoredPoint.equals(pt)Point即使作者ColoredPoint认为这是一个好主意,我们也不希望失败。


6

当古老的编程之神发明了使用类的面向对象的编程时,他们决定在组合和继承方面,对象具有两种关系:“是”和“具有”。
这部分解决了子类与父类不同的问题,但使子类在不破坏代码的情况下可用。因为子类实例“是”超类对象,并且可以直接替换,即使子类具有更多的成员函数或数据成员,所以“具有”可确保它将执行父级的所有功能并拥有其所有成员。因此,如果它们都从Point继承,则可以说Point3D“是Point”和Point2D“是Point”。另外,Point3D可以是Point2D的子类。

但是,类之间的相等性是特定于问题域的,并且上面的示例对于程序员使程序正确运行所需要的内容是模棱两可的。通常,遵循数学域规则,如果在这种情况下将比较范围限制为仅二维,但如果比较所有数据成员,则数据值将生成相等。

因此,您得到了缩小的等式表:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

通常,您会选择最严格的规则,这些规则仍然可以在问题域中执行所有必要的功能。内置的数字相等性测试出于数学目的而被设计为尽可能严格,但是如果这不是目标,则程序员有许多解决方法,包括向上/向下取整,截断,gt,lt等。 。带有时间戳的对象通常按其生成时间进行比较,因此每个实例都必须是唯一的,因此比较变得非常具体。

在这种情况下,设计因素是确定比较对象的有效方法。有时,您必须做的是对所有对象数据成员进行递归比较,而如果您有很多对象且数据成员很多,那么这将变得非常昂贵。替代方法是只比较相关的数据值,或者让对象生成其所关心的数据成员的哈希值,以便与其他类似对象进行快速比较,对集合进行排序和修剪以使比较更快,并且不占用大量CPU,并可能允许对象要剔除的数据相同,并且将指向单个对象的重复指针放在其位置。


2

规则是,只要您覆盖hashcode(),就覆盖equals(),反之亦然。这是否一个好主意取决于预期的用途。就我个人而言,我将使用不同的方法(isLike()或类似方法)来达到相同的效果。


1
可以覆盖hashCode而不覆盖等于可以。例如,可以这样做来针对相同的相等条件测试不同的哈希算法。
Patricia Shanahan

1

对于非面向公众的类,使用等效测试方法通常非常有用,该方法允许不同类型的对象在表示相同信息时将彼此视为“相等”,但是因为Java不允许类通过类来模拟每个对象其他情况下,在可能具有不同表示形式的等效对象的情况下,最好使用单个面向公众的包装器类型。

例如,考虑一个封装不可变2D double值矩阵的类。如果一种外部方法要求大小为1000的单位矩阵,则第二种方法要求对角矩阵并传递包含1000个矩阵,而第三种方法要求2D矩阵并传递1000x1000数组,其中主对角线上的元素均为1.0而其他所有零都为零,则赋予所有这三个类的对象可能在内部使用不同的后备存储(第一个具有单个字段的大小,第二个具有一千个元素的数组,第三个具有一千个1000个元素的数组),但是应该互相报告相同的内容(因为这三个都封装了一个1000x1000不变矩阵,对角线为1,其他所有位置为零)。

除了隐藏不同的后备商店类型的存在这一事实之外,包装器还将对促进比较有用,因为检查项目的等效性通常是一个多步骤的过程。询问第一个项目是否知道是否等于第二个项目;如果不知道,请询问第二个,是否知道它是否等于第一个。如果两个对象都不知道,则向每个数组询问其各个元素的内容[在决定执行慢速的单个项目比较路线之前,可能会添加其他检查]。

请注意,在这种情况下,每个对象的等效测试方法都需要返回一个三态值(“是,我是等效的”,“否,我不是等效的”或“我不知道”),因此正常的“等于”方法将不合适。尽管任何对象在被问到其他任何对象时都可以简单地回答“我不知道”,但是向对角矩阵添加逻辑(例如,不会对任何对角矩阵或对角矩阵询问主对角线上的任何元素)将大大加快此类对象之间的比较类型。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.