【问题标题】:equals and hashCode with many fields in Java?Java中有很多字段的equals和hashCode?
【发布时间】:2023-01-11 08:34:29
【问题描述】:

在 Java 应用程序中,我更喜欢在 equals 和 hashCode 方法中使用唯一字段,而不是仅添加 id 字段或所有字段。但是,我对以下几点感到困惑:

1.通过考虑 Hibernate 中的对象状态,我认为最好不要在 equals 和 hashCode 方法中使用 id 字段,对吗?

2.当一个类中有一个唯一字段时,只使用 equals 和 hashCode 方法中的一个唯一字段是否足够(id 字段除外)?

3.当类中没有除 id 字段之外的任何唯一字段时,我是否应该添加除 id 字段之外的所有字段?或者我应该只添加一些数字字段而不是添加文本字段?

【问题讨论】:

  • 您定义什么使一个类的实例等于其他实例。如果您定义相等性基于具有相同的 id,那么如果它在您的应用程序中有效就没问题。
  • 'equals' 和 'hashCode' 的答案是不一样的。对于equals,问题是对象设计之一。它是做什么的意思对象 A 和 B 是否相等?如果对象类型包含多个字段,那么在决定是否相等时通常会考虑这些字段。对于 hashCode,问题是性能。如果愿意,您可以始终对所有对象使用相同的散列码(如“1”)——因为散列码可能会发生冲突,如果它们全部发生冲突,显然不会出现功能问题。它会工作但表现不佳。唯一的要求是,如果 A 等于 B,则它们具有相同的哈希码。

标签: java spring-boot hibernate equals hashcode


【解决方案1】:

JPA 和 Hibernate 不指定或依赖实体的 equals()hashCode() 方法的任何特定语义,因此您可以做您想做的事。

好的选择

话虽如此,有一些平等的替代方案对我来说比其他任何方案都更有意义:

  1. 平等对应于对象身份。这当然是Object.equals() 提供的默认值,它可以很好地服务于实体。或者

  2. 平等对应于持久的身份。也就是说,当且仅当实体具有相同的实体类型和主键时,实体才相等。或者

  3. 平等对应于(仅)价值平等。也就是说,除了 ID 之外,所有对应的持久字段都相等。关于如何将其应用于映射关系还有其他变化。或者

  4. 平等对应于持久的身份和价值平等。同样,关于值相等部分如何应用于映射关系也存在差异。

    一般建议

    通常,您最好遵循以下经验法则:

    1. 与大多数其他类一样,尤其是可变类,默认只继承Object.equals()Object.hashCode().在你做其他事情之前要有一个特定的目的和计划,记住你只有一个选择。而且它很有影响力。

    2. 如果您确实覆盖了equals()(因此也覆盖了hashCode()),那么请在所有实体中以一致的方式进行覆盖。

    3. 在选择涉及价值平等的选项之前,请仔细考虑。对于一般的可变类来说,这通常是一个糟糕的选择,实体也不例外。

      具体问题

      1.通过考虑 Hibernate 中的对象状态,我认为最好不要在 equals 和 hashCode 方法中使用 id 字段,对吧?

      我认为使用ID很好。这只是您希望平等为您的实体代表什么的问题。您绝对可以拥有具有相同类型和 ID 的不同实体对象,并且您可能希望能够使用 equals() 检测到它。其他持久性字段可能会或可能不会影响到这一点。

      特别是,equals() 方法仅基于实体 ID 可能对映射到 Set 时出现在一对多关系的“多”端的实体有意义。

      2.当一个类中有一个唯一字段时,只使用 equals 和 hashCode 方法中的一个唯一字段是否足够(除了 来自 id 字段)?

      我认为没有充分的理由只考虑唯一字段的适当子集,除了仅由实体 ID 组成的子集。或者,如果所有字段都是唯一的,那么由所有字段组成的字段除了身份证。建议您可能能够考虑其他适当子集的逻辑围绕实体的持久身份展开,该身份完全且最好地由其 ID 表示。

      3.当类中没有除 id 字段之外的任何唯一字段时,我是否应该添加除 id 字段之外的所有字段?或者我应该只 添加一些数字字段而不是添加文本字段?

      如果您的平等感是基于实体值,那么我看不出省略任何持久字段有什么意义,可能除了 ID。不要随意省略 ID——它很可能是您想要包含的内容。同样,这取决于 equals() 对您的实体意味着什么。

【讨论】:

    【解决方案2】:

    这是一个棘手的问题,hibernate 本身没有明确的答案。

    John Bollinger 的回答涵盖了您的具体问题,但还有一些关于如何考虑平等和休眠的额外背景应该有助于弄清楚该怎么做。毕竟,鉴于 hibernate 不要求您做任何特别的事情,您可以做任何您想做的事,这导致了一个明显的问题:...好吧,那么我应该做什么呢?

    这个问题归结为(使用Person作为模型类+关联表的任意示例;此外,假设person表具有生成的单个唯一ID(随机UUID或自动排序的整数值) .

    Person 的实例代表什么

    大致有2个答案:

    • 代表一个人。 person表中的一行代表一个人;这两件事没有关系。
    • 代表person 表中的一行.
    • 它代表我的应用程序中的一个状态,仅此而已。

    尽管这些事情听起来很相似,但它们会导致关于平等的相反含义。

    哪个选择是正确的?随你(由你决定。

    继续阅读时,请记住:

    任何尚未“保存”的 Person 实例都将具有 idnull 值,因为在插入时,hibernate 将要求 DB 为其生成一个值或自己生成一个值,然后才填充它在。

    一个实例代表一行

    • 第二种模式下的平等(Person的一个实例代表表中的一行)应该看起来只要id 列,因为它定义了行的唯一性; person 表中一行的任何 2 个表示都保证引用同一行(因此,平等的) 当且仅当 id 相等时。这是一个必要条件和充分条件:如果它们相等,则 2 个对象必然引用同一行,如果它们不相等,则它们必然引用不同的行。
    • 值得注意的是,如果id仍然是null,那么他们不能相等,甚至对他们自己:更普遍的问题是:“这个对象代表行是否等于另一个对象代表行”如果这些对象代表行(未保存的行),则这是一个毫无意义的问题).如果您在每个对象上调用 save(),您最终会得到 2 行。最佳情况下,这样的对象应该被认为处于这样一种状态,即尝试对其调用 equals 是失败的,但 equals 的规范声明它们不能抛出,因此 false 是最佳答案。这意味着你想要:
    class Person {
      // fields
      @Override public boolean equals(Object other) {
        if (other == this) return true;
        if (other == null || other.getClass() != Person.class) return false;
        UUID otherId = ((Person) other).id;
        return id == null ? false : id.equals(otherId);
      }
    }
    

    这将您的 equals 方法定义为“最终代表同一行”。这成立即使你改变有意义的状态:

    • 更改名称并保存对象?它......仍然是同一行,这个平等实施反映了这一点。
    • 在比较中未保存时对每个调用 save()?然后你得到 2 行 - 这个平等实现之前反映了这一点尝试保存后。
    • 如果调用自身 (a.equals(a)),则返回 true 作为平等规范的要求;它也适用于“建模一行”视图:如果您对同一个对象调用 save() 两次,它仍然只是一行。

    一个实例代表一个人

    一个人的本质与其获得的自动序列/自动生成 ID 完全无关;我们使用 hibernate 的事实是一个实现细节,在考虑平等时根本不应该发挥作用;毕竟,这个对象代表了一个人的概念,而这个概念完全独立于数据库而存在。数据库是为人建模的一件事;这个类的实例是另一个。

    在这个模型中,您应该做完全相反的事情:找到可以唯一标识一个人本身的东西,并与之进行比较。毕竟,如果您在数据库中有 2 行都包含相同的社会安全号码,那么您只有 1 个人……而您恰好有 2 行都指的是同一个人。鉴于我们选择我们的实例来暗示它代表一个人,然后从 A 行加载的实例和从 B 行加载的实例应该被认为是相等的——毕竟,它们代表同一个人。

    在这种情况下,您编写了一个考虑所有相关字段的 equals 方法除了autoseq/autogen ID 字段!如果有一个单独的唯一 ID,例如社会安全号码,请使用它。如果没有,基本上可以归结为比较所有字段的 equals 方法,除了ID。因为那是一个与定义一个人的东西绝对没有关系的领域。

    实例定义应用程序中的状态

    这几乎是一种逃避,通常意味着平等是无关紧要的/不适用的。这就像询问如何为 InputStream 实现实现一个 equals 方法——大多数情况下,你……不会。

    在这里,默认行为(Object 自己的实现)是您想要的,因此,您既不实现hashCode也不实现equals。 Person 的任何实例都等于它自己(如 a.equals(a),相同的引用),而不等于任何其他实例,即使另一个实例的每个字段都具有相同的值,甚至 id 字段也不是 null (代表同一行)。

    这样的对象不能有意义地用作值对象。例如,将此类内容填充到哈希图中是毫无意义的(充其量,您可以将它们填充到 IdentityHashMap 中,因为这些语义适用。进行任何查找的唯一方法是使用 .put() 的引用之前进入它并用它调用.get())。

    哪一个是对的?由你决定。但要清楚地记录下来,因为根据我的经验,许多休眠用户绝对相信第一个或第二个模型是唯一的正确答案,并认为另一个答案完全是疯子。这是有问题的——他们会假设编写代码全部hibernate 模型类完全按照他们想要的方式工作,因此甚至不会考虑检查 docs/impl 来了解它的实际工作方式。

    就其价值而言,对象就是对象,数据库行并不能很好地映射到对象的概念。 SQL 和 java 的 null 概念完全不兼容,并且“查询”的概念不能很好地映射到表(在选择表达式、选择视图和 JOIN 之间,这应该是显而易见的)——hibernate 正在向风车倾斜。这是一个有漏洞的抽象,这是它的许多漏洞之一。有漏洞的抽象可能很有用,只是要注意,在“边缘”,hibernate 试图兜售你的原则(对象可以表示查询结果和行)有你会遇到的限制。很多。

    【讨论】:

      猜你喜欢
      • 2014-05-14
      • 1970-01-01
      • 2011-07-09
      • 2011-02-13
      • 2019-06-02
      • 1970-01-01
      • 1970-01-01
      • 2021-10-31
      • 1970-01-01
      相关资源
      最近更新 更多