有人可以解释一下 Intellij 的默认 equals 实现吗?

Can someone please explain Intellij's default equals implementation?

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

有问题的 class 是一个 @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);
    }

项目contains/usesSpring启动、Hibernate、Lombok。

谢谢

两个对象不相等class.

对于'preferred',这取决于'id'是什么。最后一行似乎有点多余;本来可以

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

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

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

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

工作中存在一个根本问题,JPA/Hibernate 固有的问题。对于这个例子,假设我们有一个名为 User 的数据库 table,我们有一个名为 User 的 class 对其建模。

问题归结为:

javaclassUser代表什么?它代表'a row in the database table "User"',还是代表用户?

根据您的回答,您对 equals 方法的要求截然不同。根据您选择的 equals 方法,错误地回答这个问题会导致代码错误。据我所知,没有实际的 'standard',人们只是做了一些事情,大多数人并没有意识到这是一个基本问题。

代表数据库中的一行

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

  • 如果在数据库定义中对主键列建模的所有字段在两个实例之间是相等的,那么它们是相等的,即使另一个(非主键) ) 字段不同。毕竟,这就是 DB 确定相等性的方式,因此 java 代码应该匹配它。

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

换句话说:

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

毕竟,如果您制作 2​​ 个单独的新对象,并且 save() 它们都是,它们将变成 2 个单独的行。

代表一个用户对象

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

那我选哪一个呢?

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

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

就其价值而言,我认为 'represents a row in the DB' 是更 'useful' 的解释(因为不这样做涉及调用 getter,这使得相等性检查非常昂贵,因为它可能导致数百个 SELECT 调用和一大堆堆内存,因为你拉了一半的数据库!),然而,'an instance of class User represents a user in the system' 是更像 java 的解释,也是最 java程序员会(然后错误地,如果你在这里使用 intellij 的建议)默默地假设。

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

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

注意:Lombok 是 java 而不是特定于 Hibernate 的,因此它做出 'represents a user in the system' 选择。您可以通过告诉 lombok 仅使用 id 字段(在该字段上粘贴 @EqualsAndHashCode.Include )来尝试将 lombok 推向 'row in DB' 解释,但 lombok 仍会考虑 2 null 值 / 2 0 值相同,即使它不应该。这是在休眠状态下,因为它打破了各种规则和规范。


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

为什么要调用 .getClass()

Java 对等于的含义有合理的规定。这在 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 的子class:您决定给列表一种颜色。你可以有一个蓝色的字符串列表和一个红色的字符串列表。

现在 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<T> extends ArrayList<T> { .. }。当然,红色空列表不再等于蓝色空列表对吧?

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

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));

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

有一种不同的观点认为你应该能够做出这样的 classes。就像 JPA/Hibernate 的情况一样,我们又在纠结 equals 的含义。

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

的实例

鉴于规则 a.equals(b),实现此目的的唯一方法是什么?然后 b.equals(a) 存在,是动物检查 that 的 class 和 returns false 如果它不完全是 Animal。也就是说:

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

.getClass() 检查完成了这个。

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

换句话说,回到彩色列表,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,其中 Y 是 X 的子class,但您可以通过写出 canEqual Y 中的方法。如果你写一个不增加语义负载的子class,你就会这样做。

这对解决上述有关休眠的问题没有丝毫帮助。

谁知道像平等这样看似简单的事情隐藏着 2 篇难解难懂的哲学论文,是吧?

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

我并不是想破坏 ~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

很明显你有那个文件的不同版本?或者您使用不同的模板?也许一些插件改变了它?