it-swarm.cn

在equals方法中具有不进行精确匹配的逻辑是一个好主意吗?

在协助学生进行大学项目的同时,我们进行了大学提供的Java练习),该练习使用以下字段定义了地址类别:

_number
street
city
zipcode
_

它指定如果数字和邮政编码匹配,则equals逻辑应返回true。

曾经有人告诉我,equals方法仅应在对象之间进行精确比较(检查指针之后),这对我来说很有意义,但与给出的任务相矛盾。

我知道为什么您要覆盖逻辑,以便可以将list.contains()之类的东西与您的部分匹配项一起使用,但是我想知道是否将其视为犹太洁食,如果不是,为什么呢?

35
William Dunne

定义两个对象的相等性

可以为任意两个对象任意定义相等性。没有严格的规则禁止某人定义他们想要的任何方式。但是,当平等对于正在实施的领域规则有意义时,通常会定义平等。

预期会遵循 等价关系合同

  • 它是自反:对于任何非空参考值x,x.equals(x)应该返回true。
  • 它是对称:对于任何非空参考值x和y,x.equals(y)当且仅当y.equals( x)返回true。
  • 它是传递:对于任何非空参考值x,y和z,如果x.equals(y)返回true和y.equals( z)返回true,则x.equals(z)应该返回true。
  • 它是一致:对于任何非空参考值x和y,多次调用x.equals(y)始终返回true或始终返回false,如果没有修改对象的相等比较中使用的信息。
  • 对于任何非null参考值x,x.equals(null)应该返回false。

在您的示例中,也许无需区分邮政编码和编号相同但不同的两个地址。有一些完全合理的域可以期望以下代码起作用:

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2);

如您所提到的,这在您不关心它们是不同的对象时会有用,而您只关心它们所拥有的值。也许邮政编码+街道号码足以让您标识正确的地址,而其余信息是“额外的”,并且您不希望这些额外的信息影响您的平等逻辑。

这可能是一个完美的软件建模。只要确保有一些文档或单元测试来确保此行为,并确保公共API可以反映此用法。


不要忘记hashCode()

与实现相关的一个附加细节是许多语言大量使用哈希码的概念。这些语言,Java包括,通常假定以下主张:

如果x.equals(y),则x.hashCode()和y.hashCode()相同。

通过与以前相同的链接:

请注意,通常有必要在每次重写此方法(等于)时都重写hashCode方法,以便维护hashCode方法的常规约定,该约定规定相等的对象必须具有相等的哈希码。

请注意,具有相同的hashCode并不意味着两个对象相等!

从这种意义上讲,当实现平等时,还应该实现遵循上述属性的hashCode()。数据结构使用此hashCode()来提高效率并保证其操作复杂性的上限。

提出一个好的哈希码功能很困难,而且本身就是一个完整的话题。理想情况下,两个不同对象的hashCode应该不同或在实例实例之间具有均匀分布。

但是请记住,即使以下简单实现不是“好的”哈希函数,也仍然可以实现equals属性:

public int hashCode() {
    return 0;
}

实现哈希码的一种更常见的方法是使用定义相等性的字段的哈希码并对它们进行二进制运算。在您的示例中,邮政编码和街道号码。通常这样做:

public int hashCode() {
    return this.zipCode.hashCode() ^ this.streetNumber.hashCode();
}

含糊不清时,请选择清晰度

在这里,我对人们对平等的期望有所区别。不同的人对平等有不同的期望,如果您希望遵循 最少惊讶原则 ,则可以考虑其他选择来更好地描述您的设计。

哪些应该被认为是平等的?

Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2); // Are typos the same address?
Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");
assert a1.equals(a2); // Are abbreviations the same address?
Vector3 v1 = new Vector3(1.0f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // Should two vectors that have the same values be the same?
Vector3 v1 = new Vector3(1.00000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // What is the error tolerance?

可以为每一个是对或错的情况下提出理由。如有疑问,可以定义一种不同的关系,该关系在域的上下文中更加清晰。

例如,您可以定义isSameLocation(Address a)

Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");

System.out.print(a1.equals(a2)); // false;
System.out.print(a1.isSameLocation(a2)); // true;

或使用Vector时,isInRangeOf(Vector v, float range)

Vector3 v1 = new Vector3(1.000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);

System.out.print(v1.equals(v2)); // false;
System.out.print(v1.isInRangeOf(v2, 0.01f)); // true;

这样,您可以更好地描述平等的设计意图,并且避免超出未来读者对代码实际功能的期望。 (您可以查看所有稍有不同的答案,以了解人们对您的示例的平等关系的期望如何变化)

89
Albuquerque

在大学作业的背景下,任务目的是探索和理解操作员优先。这似乎是一个示例任务,具有足够的隐含目的,使其在当时显得很有价值。

但是,如果这是我的代码审查,我会将其标记为重大的设计缺陷。

问题是这样的。它启用语法清晰的看似正确的代码:

if (driverLocation.equals(parcel.deliveryAddress)) { parcel.deliver(); }

并且根据其他用户的评论,此代码将在巴西邮政编码唯一的街道上产生正确的结果。但是,如果您随后在该假设不再有效的美国尝试使用此软件,则此代码看起来仍然正确。

如果已实现为:

if (Address.isMatchNumberAndZipcode(driverLocation, parcel.deliveryAddress)) {
  parcel.deliver();
}

然后几年后,当给另一个巴西开发人员提供代码库,并告诉该软件将包裹递送到他们在加利福尼亚的新客户的错误地址时,现在打破的假设在代码中显而易见,并且可以在决策点看到。是否交付-这很可能是维护程序员检查包裹为什么被交付到错误地址的第一位。

在运算符重载中隐藏不明显的逻辑将使代码修复花费更长的时间。要在此代码中捕获此问题,可能需要与调试器一起逐步完成代码。

42
Michael Shaw

平等是一个背景问题。与是否涉及两个对象一样,两个对象是否被视为相等也是一个上下文问题。

因此,if在您的上下文中,可以忽略城市和街道,那么仅基于邮政编码和数字来实现平等就没有问题。 (正如其中一项评论中指出的那样,邮政编码和数字are足以唯一标识巴西的地址。)

当然,您应该确保遵循正确的规则来重载相等,例如确保您也相应地重载hashCode

25
Jörg W Mittag

由于您认为有用的考虑,当且仅当应将两个对象视为相等时,相等运算符才会声明两个对象相等。

我会重复一遍:由于您认为有用的任何考虑因素。

软件开发人员坐在这里。除了符合明显的要求(a = a,a = b表示b + a,a = b和b = c表示a = c)以及与哈希函数的一致性之外,相等运算符可以是您喜欢的任何东西。

3
gnasher729

尽管给出了许多答案,但我的意见仍然不存在。

曾经有人告诉我,equals方法只能在对象之间进行精确比较

除了规则所说的以外,这个定义是人们在谈论平等时出于直觉所假定的。一些答案说平等取决于环境。从某种意义上说,它们是正确的,即使它们的所有字段都不匹配,对象也可以相等。但是,对“相等”的共识不应重新定义太多。

回到主题,对我来说,如果一个地址指向相同的位置,则该地址等于另一个地址。

在德国,城市名称可能有不同,例如,郊区的名称。然后,可以将郊区SUB中的地址所在的城市仅指定为“主要城市”,也可以仅指定“主要城市,SUB”甚至“ SUB”。因为可以指定主要城市名称,所以城市及其所有分配的郊区中的所有街道名称必须唯一。

即使城市名称有所不同,此处的邮政编码也足以告诉城市。
但是离开这条街并不是唯一的,除非Zip代码也指向一条众所周知的街道,但通常并非如此。
因此,如果两个地址可以指向不同的位置(其差异由被忽略的字段组成),则认为两个地址相等是不直观的。

如果存在只需要部分但全部字段的用例,则应适当地命名执行此方法的compare方法。只有一种“等于”方法不应秘密地转换为“仅在一种特殊用例中等于-但没人能看到”。

这意味着,出于解释原因,我会说...

但我想知道这是否被视为犹太洁食

如果您不小心遇到了街道名称无关紧要的地方,那就不知道了:不,这无关紧要。
如果您要编写的程序不仅要在这样的位置使用,不,不是。
如果您想让学生有做正确事的感觉,并使代码易于理解和合乎逻辑:不,不是。

2
puck

尽管给定的要求与人类的意义相矛盾,但是让对象属性的子集定义“唯一”的含义是可以的。

这里的问题是equals()hashcode()之间存在技术关系,因此对于两个对象ab被认为是:
if a.equals(b) then a.hashcode()==b.hashcode()
如果您有一部分属性定义了唯一性条件,则必须使用相同的子集来计算hashcode()的返回值。

毕竟,满足要求的更合适的方法可能是实现Comparable甚至是自定义isSame()方法。

1
Timothy Truckle

取决于。

这是个好主意吗??这取决于。例如,如果您正在开发将在--uni []分配中使用仅一次的应用程序(如果要在分配审查后将代码丢弃),则可能是个好主意。一些迁移实用程序(您只需迁移一次旧数据,就不再需要该实用程序了)。

但是在很多情况下,在IT行业中这是个坏主意。为什么? @JörgW Mittag说平等是一个背景问题...如果在您的背景下有意义...。但是通常在许多不同的上下文中使用相同的对象,它们具有不同的平等观点。仅举几个例子说明如何定义同一实体的相等性:

  • 作为两个实体的all属性的相等性
  • 作为两个实体的主键相等
  • 等同于两个实体的主键和版本
  • 除主键和版本外,所有“业务”属性均相等

如果您在equals()中实现某个特定上下文的逻辑,则稍后将很难在其他上下文中使用该对象,因为您项目团队中的许多开发人员将无法完全了解该对象。在那里确切地实现了上下文的逻辑,在什么情况下它们可以依靠它。在某些情况下,他们会错误地使用它(如@Michael Shaw所述),在其他情况下,他们将忽略逻辑并出于相同的目的实现自己的方法(可能与您预期的工作方式不同)。

如果要使用您的应用程序使用时间较长(例如2-3年),通常会有多个新要求,多个更改和多个上下文。对平等的期望很可能会multiple不同。这就是为什么我建议:

  • 正式实现equals(),无需连接业务上下文,意味着无需任何业务逻辑,就像所有对象属性的均等一样(当然必须遵循hashCode/equals合同)
  • 为每个上下文提供一个单独的方法,该方法在此上下文中实现相等性,例如isPrimaryKeyAndVersionEqual()areBusinessAttributesEqual()

然后要在特定上下文中查找对象,只需使用相应的方法,如下所示:

if (list.sream.anyMatch(e -> e.isPrimaryKeyAndVersionEqual(myElement))) ...

if (list.sream.anyMatch(e -> e.areBusinessAttributesEqual(myElement))) ...

这样,代码中的错误将更少,代码分析将变得更加容易,针对新要求的应用程序更改将变得更加容易。

1
mentallurg

像其他提到的一样,一方面平等只是满足某些属性的数学概念(例如参见 Albuquerque的 答案)。另一方面,其语义和实现由上下文决定。

无论实现细节如何,都以代表算术表达式的类为例(例如(1 + 3) * 5)。如果使用算术表达式的标准评估规则为此类表达式实现解释器,则考虑(1 + 3) * 510 + 10设为equal。但是,如果您为此类表达式实现漂亮的打印机,则上述实例将不被认为equal,而(1 + 3) * 5(1+3)*5 将。

0
michid

正如其他人所提到的,对象相等的确切语义是业务域定义的一部分。在这种情况下,我认为拥有“ Address(包含numberstreetcityzipcode)来定义一个非常狭窄的平等定义(例如,正如其他人提到的那样,它在巴西有效,但在美国无效)。

相反,我将使Address具有类似值的语义来表示相等性(由所有成员的相等性定义)。然后,我要么:

  1. 创建一个StreeNumberAndZip类(_# TODO: bad name_),该类仅包含streetzipCode,并在其上定义equals。每当您想以特定方式比较两个Address对象时,都可以执行addressA.streetNumberAndZip().equals(addressB.streetNumberAndZip())或...
  2. 使用bool equalStreeNumberAndZipCode(Address a, Address b)方法创建AddressUtils类,该方法定义了那里的窄等式。

在这两种情况下,您仍然可以使用addressA.equals(addressB)进行完全相等性检查。

对于对象的n字段,有_2^n_个不同的相等定义(可以在检查中包括或排除每个字段)。如果您发现自己需要以许多不同的方式检查相等性,则使用_enum AddressComponent_之类的内容也可能很有用。然后,您可以使用bool addressComponentsAreEqual(EnumSet<AddressComponent> equatedComponents, Address a, Address b),因此您可以调用类似

_bool addressAreKindOfEqual = AddressUtils.addressComponentsAreEqual(
    new EnumSet.of(
        AddressComponent.streetNumber, 
        AddressComponent.zipCode,
    ),
    addressA, addressB
);
_

显然,这要输入更多的内容,但是可以避免使用相等性检查方法呈指数级增长的情况。

0

平等是微妙的,正确的意义远非如此。特别是在实现平等运算符的语言中,突然意味着您的对象应该与集合和映射一起玩Nice。

在绝大多数情况下,平等应该是同一性,这意味着一个对象只有在相同的内存与相同的地址。身份关系始终遵守适当的平等关系的所有条件:自反性,可传递性等。身份也是比较任何两件事的最快方法,因为您只比较两个指针即可。尊重等价关系合同是任何实现平等的最重要的事情,因为不这样做会转化为臭名昭著的难以诊断的错误。

实现equals的第二种方法是比较类型是否匹配,然后比较对象的每个“拥有”字段。这通常最终导致重复深入每个对象的细节。如果您的对象进入调用equals的数据结构,则使用此方法时,equals可能就是数据结构大部分时间所用的对象。还有其他问题:

  • 如果对象改变了,那么它与其他对象比较的结果也会改变,这打破了标准类对相等性的各种假设。
  • 如果您的对象位于类/接口层次结构中,则比较该层次结构中两个对象的唯一明智的方法是它们的具体类型是否完全匹配(请参见Joshua Bloch出色的有效Java预订以获取更多详细信息);
  • 如果您尝试通过包含尽可能多的字段来使平等关系变得非常严格,那么最终您将最终陷入平等不符合“相同”业务逻辑的情况。

第三种方法是仅选择与业务逻辑相关的字段,而忽略其余字段。这种方法被破坏的可能性任意接近1。其他人提到的第一个原因是,在one上下文中比较有意义在all上下文中一定有意义。该语言要求您定义一个形式相等,因此它可以在所有情况下更好地工作。对于地址,根本不存在这种比较逻辑。您可以使用专门的“这两个地址look相同”的方法,但是您不应该冒任何这种方法被语言支持的唯一真实方式进行比较的风险这将不可避免地使读者感到困惑。

我还建议您看看Falsehoods程序员相信地址: https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/ 读起来很有趣,可能会帮助您避免一些陷阱。

0
Kafein