【问题标题】:Can someone please explain Intellij's default equals implementation?有人可以解释一下 Intellij 的默认 equals 实现吗?
【发布时间】:2021-12-08 04:23:20
【问题描述】:

在使用 lombok 的 @Data 注释时,我从 IntelliJ IDEA 那里得到了这个建议。

有问题的类是一个@Entity。 谁能解释一下:

  1. 它究竟做了什么(尤其是带有Hibernate的部分)
  2. 这种方法是否优于逐个比较每个字段?如果是,为什么?
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o))
            return false;
        MyObject that = (MyObject ) o;
        return id != null && Objects.equals(id, that.id);
    }

项目包含/使用 Spring boot、Hibernate、Lombok。

谢谢

【问题讨论】:

  • 您似乎也想知道 getClass() 的东西。我编辑了我的答案以解释那里发生了什么。不幸的是,这是第二次复杂的深潜。需要很多背景知识才能知道要求是什么。

标签: java spring-boot hibernate intellij-idea lombok


【解决方案1】:

如果两个对象属于不同的类,则它们不相等。

对于“首选”,它取决于“id”是什么。最后一行似乎有点多余;本来可以

return Objects.equals(id, that.id);

因为 null 情况由 Objects.equals 处理。但依我的口味,写起来更清楚

return id != null && id.equals(that.id);

额外的层没有添加我在示例中看到的任何内容。

【讨论】:

  • 这很清楚。但是,它为什么要使用 Hibernate.getClass()?
  • 这个答案很糟糕。 不,它们不相同Objects.equals(null, null) 为真,而对于id != null && id.equals(that.id),如果this.idthat.id 均为null,则为假。这是故意的(请参阅我给出背景的答案,这很复杂)。
  • 如果 null != null 对我来说似乎很奇怪。即,我认为这是一个错误,“代表一个用户,而不是一行”的情况,但这当然会引发比较对象的“其他”字段的问题。
【解决方案2】:

工作中存在一个基本问题,这是 JPA/Hibernate 固有的问题。对于此示例,假设我们有一个名为 User 的 db 表,并且我们有一个也名为 User 的类对其进行建模。

问题归结为:

java类User代表什么?它是代表'数据库表“用户”中的一行',还是代表一个用户?

根据您的回答,您对 equals 方法有完全不同的要求。根据您选择的 equals 方法,错误地回答此问题会导致代码错误。据我所知,没有真正的“标准”,人们只是在做某事,而大多数人并没有意识到这是一个基本问题。

代表数据库中的一行

这样的解释将建议您的 equals 方法的以下实现:

  • 如果在数据库定义中模拟主键列的所有字段在两个实例之间都相等,那么它们是相等的,即使其他(非主键)字段不同。毕竟数据库是这样判断相等的,所以java代码应该匹配它。

  • Java 代码在处理 NULL 时应该像 SQL。也就是说,几乎所有的等式定义、equals方法代码生成器(包括lombok、intellij、 eclipse),甚至Objects.equals方法,都非常不同,在这种模式下,null == null 应该为 FALSE,就像在 SQL 中一样! 具体来说,如果任何主键字段具有空值,则该对象不能等于任何其他对象,即使是抄本本身;为了遵守 java 规则,它可以(必须,真的)等于它自己的引用。

换句话说:

  • 如果 [A] 它们实际上是同一个对象 (this == other) 或 [B] 两个对象的 unid 字段已初始化且相等,则任何 2 个对象都是相等的。无论您使用 null 还是 0 来跟踪“尚未写入数据库”,该值都会立即取消该行与任何其他行相等的资格,即使是另一个具有 100% 相同值的行。

毕竟,如果您创建 2 个单独的新对象并 save() 两者,它们将变成 2 个单独的行。

它代表一个用户对象

然后发生的情况是,equals 规则执行 180。主键,假设它是 unid 风格的主键而不是自然主键,本质上是一个实现细节。想象一下,不知何故,在您的数据库中,您最终得到了完全相同的用户的 2 行(可能有人搞砸了,并且未能在用户名上添加 UNIQUE 约束)。在系统上用户的语义模型中,用户是由用户名唯一标识的,因此,相等性仅由用户名定义。 2 个具有相同用户名但不同 unid 值的对象仍然相等

那我该拿哪一个?

我不知道。幸运的是,您的问题要求解释而不是答案!

IntelliJ 告诉您的是使用第一个解释(数据库中的行),甚至正确地应用了不可靠的 null 东西,所以在 intellij 中编写建议工具的人至少似乎理解发生了什么。

对于它的价值,我认为“代表数据库中的一行”是更“有用”的解释(因为不这样做涉及调用 getter,这会使相等性检查非常昂贵,因为它可能会导致数百次 SELECT 调用和一大堆堆内存,当你把一半的数据库拉进去时!),然而,“User 类的一个实例代表系统中的一个用户”是更像 java 的解释,也是大多数 java 程序员会的解释(错误的话,如果你在这里使用intellij的建议)默默地假设。

我已经在自己的编程工作中解决了这个问题,首先从不使用 hibernate/JPA,而是使用 JOOQ 或 JDBI 等工具。但是,缺点是通常你会得到更多的代码——你有时确实有一个对象,例如称为UserRow,代表一个用户行和一个对象,例如称为User,代表系统上的用户。

另一个技巧可能是决定将所有 Hibernate 模型类命名为 XRow。名称很重要,并且是最好的文档:这毫不含糊,并且在此代码的所有用户中都提供了有关如何解释其语义含义的线索:DB 中的行。因此,intellij 建议将是您的 equals 实现。

注意:Lombok 是 java 而不是特定于 Hibernate,因此它选择了“代表系统中的用户”。您可以通过告诉 lombok 仅使用 id 字段(在该字段上粘贴 @EqualsAndHashCode.Include)来尝试将 lombok 推向“DB 中的行”解释,但 lombok 仍会考虑 2 null 值 / 2 0 值相同,即使它不应该。这是在休眠状态,因为它违反了各种规则和规范。


(注意:由于对另一个答案的评论而添加)

为什么会调用.getClass()

Java 对 equals 的含义有合理的规则。这是在 equals 方法的 javadoc 中,可以依赖这些规则(例如,HashSet 和 co)。规则是:

  • 如果aequals(b) 为真,a.hashCode() == b.hashCode() 也必须为真。
  • a.equals(a) 必须为真。
  • 如果a.equals(b)b.equals(a) 也必须为真。
  • 如果a.equals(b)b.equals(c)a.equals(c) 也必须为真。

明智而简单,对吧?

不。这实际上非常复杂。

假设您创建了ArrayList 的子类:您决定为列表赋予颜色。你可以有一个蓝色的字符串列表和一个红色的字符串列表。

现在 ArrayList 的相等方法检查 that 是否是一个列表,如果是,则比较元素。看起来很明智,对吧?我们可以看到它的实际效果:

List<String> a = new ArrayList<String>();
a.add("Hello");
List<String> b = new LinkedList<String>();
b.add("Hello");
System.out.println(a.equals(b));

这是真的。

现在让我们实现彩色数组列表:class ColoredList&lt;T&gt; extends ArrayList&lt;T&gt; { .. }。当然,红色的空列表不再等于蓝色的空列表,对吧?

不行,如果你这样做,你就违反了规则!

List<String> a = new ArrayList<String>();
List<String> b = new ColoredList<String>(Color.RED);
List<String> c = new ColoredList<String>(Color.BLUE);
System.out.println(a.equals(b));
System.out.println(a.equals(c));
System.out.println(b.equals(c));

打印 invalid 的 true/true/false。结论是,实际上不可能创建任何添加一些语义相关信息的列表子类。唯一可以存在的子类是那些主动破坏规范(坏主意),或者其添加对平等没有影响的子类。

有一种不同的观点认为你应该能够制作这样的课程。就像在 JPA/Hibernate 案例中一样,我们再次纠结于 equals 的含义。

equals 实现的一个更常见和更好的默认行为是简单地声明任何 2 个对象只有在它们具有完全相同的类型时才能相等:Dog 的实例不能等于 @ 的实例987654358@.

鉴于规则a.equals(b)?那么b.equals(a) 存在,是动物检查that 的类并返回false,如果它不完全是Animal。换句话说:

Animal a = new Animal("Betsy");
Cow c = new Cow("Betsy");
a.equals(c); // must return false!!

.getClass() 检查完成此操作。

Lombok 为您提供两全其美的体验。它不能创造奇迹,所以它不会取消在类型级别需要选择可扩展性的规则,但是 lombok 有canEqual 系统来处理这个问题:Animal 的 equals 代码会询问that 代码如果两者可以相等。在这种模式下,如果你有一些非语义不同的动物子类(例如ArrayList,它是 AbstractList 的子类,完全不改变语义,它只是添加了与相等性无关的实现细节),它可以说它可以相等,而如果你有一个语义不同的,比如你的彩色列表,它可以说没有。

换句话说,回到彩色列表,IF ArrayList 和 co 是用 lombok 的 canEqual 系统编写的,这本来可以解决的,你可以得到结果(a 是数组列表,b 是红色列表,c 是模糊列表):

a.equals(b); // false, even though same items
a.equals(c); // false, same reason.
b.equals(c); // false and now it's not a conflict.

Lombok 的默认行为是所有子类型都添加语义负载,因此任何 X 不能等于 Y 是 X 的子类的任何 Y,但您可以通过在 Y 中写出 canEqual 方法来覆盖它。如果你编写一个不增加语义负载的子类,你会这样做。

这对上面关于休眠的问题一点帮助也没有。

谁知道像平等这样看似简单的事情却隐藏着两篇棘手的哲学论文,嗯?

有关 canEqual 的更多信息,see lombok's @EqualsAndHashCode documentation

【讨论】:

  • 哇。这为未知提供了一些启示。老实说,我不信任 IDE(尽管它在这个用例中是正确的),因为我想确保在获取和/或写入数据库时​​避免任何与休眠相关的错误(因为这些错误会使它变得棘手找出问题的根源)。现在很明显,我将使用您所描述的 它表示 DB 中的一行 方法。感谢您非常详细的回答亲切的陌生人!
  • 为什么它使用 Hibernate.getClass() 而不是 Object.getClass() 呢?
  • @ursokte 因为 hibernate 有点像“类黑客”——它制作代理,就 JVM 而言,它在技术上是子类,以便制作例如getCoursesTaken() 实际上在后台执行一些 SELECT 语句。 Hibernate.getClass 获取被代理的类。 Lombok 的 canEqual 是针对这个问题的更通用的解决方案。
【解决方案3】:

我并不是想破坏 ~rzwitserloot 的出色答案,只是想帮助您弄清楚为什么它为您使用 Hibernate.getClass(this) 而不是 this.getClass()。

它不适合我,但无论如何我的项目中都没有 Hibernate。 代码是使用速度宏生成的,如下所示:

IntelliJ 默认使用文件“equalsHelper.vm”。我在https://github.com/JetBrains/intellij-community/blob/master/java/java-impl/src/com/intellij/codeInsight/generation/equalsHelper.vm

找到了该文件版本的可能来源

它包含这个:

#macro(addInstanceOfToText)
  #if ($checkParameterWithInstanceof)
  if(!($paramName instanceof $classname)) return false;
  #else
  if($paramName == null || getClass() != ${paramName}.getClass()) return false;
  #end
#end

那么显然您有该文件的不同版本?或者你使用不同的模板?也许是某个插件改变了它?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-22
    • 2016-12-12
    • 2011-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多