Hibernate 对何时/如何覆盖 documentation 中的 equals()
/ hashCode()
有一个很好的详细描述
它的要点是,如果您的实体将成为 Set
的一部分,或者您要分离/附加其实例,您只需要担心它。后者并不常见。前者通常最好通过以下方式处理:
基于业务键的 equals() / hashCode() - 例如,在对象(或至少,会话)生命周期内不会更改的属性的唯一组合。如果以上是不可能的,则在主键上设置 equals() / hashCode() ,否则设置对象身份 / System.identityHashCode() 。这里的重要部分是您需要在将新实体添加到 Set 并持久化后重新加载;否则您可能会遇到奇怪的行为(最终导致错误和/或数据损坏),因为您的实体可能被分配到与其当前 hashCode() 不匹配的存储桶。
我不认为接受的答案是准确的。
要回答原始问题:
对于大多数情况,默认实现是否足够好?
答案是肯定的,在大多数情况下是这样。
如果实体将在 Set
中使用(这很常见)AND 实体将从中分离并随后重新附加到,则您只需要覆盖 equals()
和 hashcode()
,休眠会话(这是休眠的不常见用法)。
接受的答案表明,如果任一条件为真,则需要覆盖这些方法。
最好的 equals
和 hashCode
实现是当您使用唯一的业务密钥或自然标识符时,如下所示:
@Entity
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(unique = true, updatable = false)
private String name;
@Override
public int hashCode() {
HashCodeBuilder hcb = new HashCodeBuilder();
hcb.append(name);
return hcb.toHashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Company)) {
return false;
}
Company that = (Company) obj;
EqualsBuilder eb = new EqualsBuilder();
eb.append(name, that.name);
return eb.isEquals();
}
}
业务密钥应该在所有实体状态转换(瞬态、附加、分离、删除)中保持一致,这就是为什么你不能依赖 id 来实现平等。
另一种选择是切换到使用由应用程序逻辑分配的 UUID 标识符。这样,您可以将 UUID 用于 equals
/hashCode
,因为在刷新实体之前分配了 ID。
您甚至可以为 equals
和 hashCode
使用实体标识符,但这要求您始终返回相同的 [hashCode
值,以确保实体 hashCode 值在所有实体状态转换中保持一致,如下所示:
@Entity(name = "Post")
@Table(name = "post")
public class Post implements Identifiable<Long> {
@Id
@GeneratedValue
private Long id;
private String title;
public Post() {}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Post))
return false;
Post other = (Post) o;
return id != null &&
id.equals(other.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
//Getters and setters omitted for brevity
}
BaseEntity
中,不再考虑该问题。它在 db 端需要一些空间,但你最好为舒适付出这个代价:)
当通过延迟加载加载实体时,它不是基类型的实例,而是javassist生成的动态生成的子类型,因此对同一类类型的检查会失败,所以不要使用:
if (getClass() != that.getClass()) return false;
改为使用:
if (!(otherObject instanceof Unit)) return false;
这也是一个很好的做法,如 Implementing equals in Java Practices 中所述。
出于同样的原因,直接访问字段可能不起作用并返回 null,而不是基础值,因此不要对属性使用比较,而是使用 getter,因为它们可能会触发加载基础值。
是的,这很难。在我的项目中,equals 和 hashCode 都依赖于对象的 id。这个解决方案的问题是,如果对象还没有被持久化,它们都不起作用,因为 id 是由数据库生成的。就我而言,这是可以容忍的,因为在几乎所有情况下,对象都会立即持久化。除此之外,它工作得很好并且很容易实现。
在 Hibernate 5.2 的文档中,它说您可能根本不想实现 hashCode 和 equals - 取决于您的情况。
一般来说,从同一个会话加载的两个对象如果在数据库中相等,则它们将相等(不实现 hashCode 和 equals)。
如果您使用两个或更多会话,它会变得复杂。在这种情况下,两个对象的相等性取决于您的 equals 方法实现。
此外,如果您的 equals 方法正在比较仅在第一次持久化对象时生成的 ID,您将遇到麻烦。当调用 equals 时,它们可能还不存在。
引用文章中的一句重要的话:
我们建议使用业务键相等来实现 equals() 和 hashCode()。业务键相等意味着 equals() 方法仅比较形成业务键的属性,该键将识别我们在现实世界中的实例(自然候选键):
简单来说
public class Cat {
...
public boolean equals(Object other) {
//Basic test / class cast
return this.catId==other.catId;
}
public int hashCode() {
int result;
return 3*this.catId; //any primenumber
}
}
如果您碰巧覆盖了 equals
,请确保您履行其合同:-
对称
反光的
及物
持续的
非空
并覆盖 hashCode
,因为它的合同依赖于 equals
实现。
Joshua Bloch(Collection 框架的设计者)强烈要求遵守这些规则。
项目 9:当你覆盖 equals 时,总是覆盖 hashCode
如果您不遵守这些合同,则会产生严重的意外影响。例如,List#contains(Object o)
可能会返回错误的 boolean
值,因为一般合同未履行。
refresh()
?遵守Set
合同的实体如何最终进入错误的存储桶(假设您有足够好的哈希码实现)。Set.contains(entity)
,您将返回false
。 get() / put() / 等也是如此...