21-04-27
banq
Java中的所有类均继承自java.lang.Object,它有equals()和hashCode()方法,这两个方法是你定义自己的类时通常应该重写两个重要方法。
equals()比较两个个对象以检查它们是否代表同一对象是很重要的;如果将对象放在HashMap或HashSet中,hashCode()则很重要。它提供了那些数据结构所使用的哈希值。
即使您不了解领域驱动设计,您也可能听说过实体和值对象。如果您还没有,请简要回顾一下它们之间的区别:
- 实体:在应用程序域中具有唯一标识的对象。例如,User或Invoice。
- 值对象:仅因它们表示的值而重要的对象。例如,一个Money或Temperature对象。通常,这些对象是不可变的。
值对象的equals和hashcode方法
public class Temperature { private final double value; private final Unit unit; public Temperature(double value, Unit unit) { this.value = value; this.unit = unit; } public double getValue() { return value; } public Unit getUnit() { return unit; } enum Unit { KELVIN, CELCIUS, FAHRENHEIT; } } |
对于值对象,我们想说明所有属性相等时对象是相等的。equals()实现应该是这样的:
public class Temperature { ... @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Temperature that = (Temperature) o; return Double.compare(that.value, value) == 0 && unit == that.unit; } @Override public int hashCode() { return Objects.hash(value, unit); } ... } |
- 如果传入的对象与当前对象是相同的引用(在内存中),则相同。
- 一个对象永远不能等于null也不能等于另一个类的对象。
- 我们可以安全地转换传入的对象,因为我们确定它与该对象属于同一类。
- 比较传入对象和当前对象的每个属性
- 使用JDKObjects.hash()方法使用当前对象的所有属性生成哈希码。
现在我们可以验证Temperature具有相同属性的2个对象是否相等:
@Test void testEqualTemperature() { Temperature temperature1 = new Temperature(37.0, Temperature.Unit.CELCIUS); Temperature temperature2 = new Temperature(37.0, Temperature.Unit.CELCIUS); boolean equal = temperature1.equals(temperature2); assertTrue(equal); } |
测试hashCode()实现:
@Test void testHashCodeForEqualObjects() { Temperature temperature1 = new Temperature(37.0, Temperature.Unit.CELCIUS); Temperature temperature2 = new Temperature(37.0, Temperature.Unit.CELCIUS); int hashCode1 = temperature1.hashCode(); int hashCode2 = temperature2.hashCode(); assertThat(hashCode1).isEqualTo(hashCode2); } |
实体的equals和hashcode方法
对于实体而言,真正重要的是标识符。我们希望看到2个实例具有与同一事物相同的标识符,即使其他属性不同也是如此。假设这个简单的User实体:
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class User { @Id @GeneratedValue private Long id; private String name; protected User() { } public User(String name) { this.name = name; } public Long getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } |
由于我们只关心该id领域,因此一个简单的实现看起来像这样:
@Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User user = (User) o; return Objects.equals(id, user.id); } @Override public int hashCode() { return Objects.hash(id); } |
不幸的是,这是错误的。问题在于该id字段是由数据库生成的,并且仅在对象持久化之后才填写。因此,对于同一个对象,id最初是null,然后在将其存储在数据库中之后获得某个值。
幸运的是,Vlad Mihalcea向我们展示了如何正确实现这一点:
@Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User user = (User) o; return id != null && id.equals(user.id); } @Override public int hashCode() { return getClass().hashCode(); } |
两个重要注意事项:
- 如果id填充,我们会看到User等同的的情况,如果在两个User实例没有被存储在数据库中,则永远不会相等。
- Hashode使用硬编码的值,因为在创建对象的时间和将其保留在数据库中的时间之间不允许hashCode值发生变化。
请参阅如何使用JPA实体标识符(主键)实现equals和hashCode以获得有关此内容的更详细信息。
数据库主键生成的实体的两个方法
如果你不喜欢,上面JPA实体实现equals()和hashCode()的方式,那么就可以采取不同的路线。在创建对象之前生成主键时,有两个优点:
- id可在构造函数中赋值,你不能创建“无效”对象。
- equals()和hashCode()方法可以简化为仅考虑id。
import org.springframework.util.Assert; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Book { @Id private Long id; private String name; protected Book() { } public Book(Long id, String name) { Assert.notNull(id, "id should not be null"); Assert.notNull(name, "name should ot be null"); this.id = id; this.name = name; } public Long getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } |
Book实体没有@GeneratedValue注释,因此我们将需要在构造时传递一个值。
现在我们知道该id字段永远不会null,我们可以使用以下实现:
@Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Book book = (Book) o; return id.equals(book.id); } @Override public int hashCode() { return Objects.hash(id); } |
测试代码:
@Test void testEquals() { Book book1 = new Book(1L, "Taming Thymeleaf"); Book book2 = new Book(1L, "Taming Thymeleaf"); assertThat(book1).isEqualTo(book2); } @Test void testEquals() { Book book1 = new Book(1L, "Taming Thymeleaf"); Book book2 = new Book(1L, "Totally different title"); assertThat(book1).isEqualTo(book2); } |
测试equals和hashCode实现
为确保正确实现您的方法,请使用EqualsVerifier库包。
将其添加到您的pom.xml:
<dependency> <groupId>nl.jqno.equalsverifier</groupId> <artifactId>equalsverifier</artifactId> <version>3.6</version> <scope>test</scope> </dependency> |
并编写测试:
@Test public void equalsContract() { EqualsVerifier.forClass(Temperature.class).verify(); } |
这将测试是否equals()是自反的,对称的,可传递的和一致的。它还会测试是否hashCode()遵守java.lang.ObjectAPI中定义的合同。
结论
要正确实现equals()and hashCode(),首先确定您的对象是值对象还是实体很重要。如果是其中之一,则可以遵循博客中列出的规则。如果两者都不是(例如Controller,Service,Repository,...),那么你可能不希望覆盖的方法。