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

urs*_*kte 6 java hibernate intellij-idea lombok spring-boot

当我使用 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);
    }
Run Code Online (Sandbox Code Playgroud)

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

谢谢

rzw*_*oot 9

工作中存在一个基本问题,这是 JPA/Hibernate 固有的问题。对于这个例子,假设我们有一个名为 的数据库表User,并且我们有一个也命名User为它的模型的类。

\n

问题归结起来很简单:

\n

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

\n

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

\n

它代表数据库中的一行

\n

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

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

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

    \n
  • \n
\n

换句话说:

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

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

\n

它代表一个用户对象

\n

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

\n

那么我该选哪一个呢?

\n

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

\n

IntelliJ 告诉您的是遵循第一个解释(数据库中的行),甚至null正确应用那些奇怪的东西,因此无论谁在 intellij 中编写建议工具,至少似乎都了解发生了什么。

\n

就其价值而言,我认为“代表数据库中的一行”是更“有用”的解释(因为不这样做涉及调用 getter,这使得相等性检查的成本非常昂贵,因为它可能会导致数百次 SELECT 调用当你拉入一半的数据库时,会产生一大堆堆内存!),然而,“类的实例User代表系统中的用户”是更像java的解释,也是大多数java程序员会做的解释(那么错误的是,如果你在这里使用intellij的建议)默默地推测。

\n

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

\n

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

\n

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

\n
\n

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

\n

为什么会.getClass()被调用?

\n

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

\n
    \n
  • 如果aequals(b)为真,a.hashCode() == b.hashCode()则也必定为真。
  • \n
  • a.equals(a)一定是真的。
  • \n
  • 如果a.equals(b)那么b.equals(a)也一定是真的。
  • \n
  • Ifa.equals(b)b.equals(c)thena.equals(c)也必须为真。
  • \n
\n

明智又简单,对吗?

\n

没有。这实际上非常复杂。

\n

想象一下,您创建了 的子类ArrayList:您决定为列表指定颜色。您可以有一个蓝色的字符串列表和一个红色的字符串列表。

\n

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

\n
List<String> a = new ArrayList<String>();\na.add("Hello");\nList<String> b = new LinkedList<String>();\nb.add("Hello");\nSystem.out.println(a.equals(b));\n
Run Code Online (Sandbox Code Playgroud)\n

这打印的是真的。

\n

现在让我们实现彩色数组列表class ColoredList<T> extends ArrayList<T> { .. }:当然,红色空列表不再等于蓝色空列表,对吗?

\n

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

\n
List<String> a = new ArrayList<String>();\nList<String> b = new ColoredList<String>(Color.RED);\nList<String> c = new ColoredList<String>(Color.BLUE);\nSystem.out.println(a.equals(b));\nSystem.out.println(a.equals(c));\nSystem.out.println(b.equals(c));\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

对于事物有不同的看法,认为你应该能够开设这样的课程。就像 JPA/Hibernate 的情况一样,我们再次为 equal 的含义而苦苦挣扎。

\n

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

\n

鉴于规则,实现这一点的唯一方法a.equals(b)?然后b.equals(a)存在的是,动物检查 的类,如果不完全是 则that返回。换句话说:falseAnimal

\n
Animal a = new Animal("Betsy");\nCow c = new Cow("Betsy");\na.equals(c); // must return false!!\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

换句话说,回到彩色列表,如果ArrayList 和 co 是用 lombok 的 canEqual 系统编写的,这可能已经解决了,你可能已经得到了结果(其中a是 arraylist,b是红色列表,并且c是模糊列表):

\n
a.equals(b); // false, even though same items\na.equals(c); // false, same reason.\nb.equals(c); // false and now it\'s not a conflict.\n
Run Code Online (Sandbox Code Playgroud)\n

Lombok 的默认行为是所有子类型都会添加语义负载,因此任何 X 都不能等于任何 Y,其中 Y 是 X 的子类,但您可以通过canEqualY. 如果您编写一个不添加语义负载的子类,您就会这样做。

\n

这对您解决上述有关休眠的问题没有任何帮助。

\n

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

\n

有关 canEqual 的更多信息,请参阅 lombok 的@EqualsAndHashCode文档

\n