章
目
录
学习Java中的hashCode()
和equals()方法,它们的默认实现以及如何正确地重写它们。此外,我们将学习如何使用第三方类HashCodeBuilder
和EqualsBuilder
来实现这些方法。
1.hashCode()和equals()方法
- equals(Object otherObject) – 验证两个对象是否相等。它的默认实现只是检查两个对象的对象引用以验证它们的相等性。默认情况下,只有当两个对象引用相同的内存位置时,它们才被视为相等。大多数Java类会重写此方法以提供自己的比较逻辑。
- hashCode() – 返回对象在运行时的唯一整数值。默认情况下,整数值是从堆中对象的内存地址派生的(但这不是强制性的)。对象的哈希码用于确定将该对象存储在类似哈希表的数据结构中时的索引位置。
1.1. hashCode()和equals()之间的契约
通常在重写equals()时,也需要重写hashCode()以保持hashCode()方法的一般契约,该契约规定相等的对象必须具有相等的哈希码。
- 在Java应用程序的执行过程中,如果多次对同一对象调用hashCode(),则hashCode()必须始终返回相同的整数,前提是未修改用于equals()比较的对象的信息。 这个整数在同一应用程序或程序的两次执行之间不必保持一致。
- 如果两个对象根据equals()方法是相等的,那么对这两个对象的每一个调用hashCode()必须产生相同的整数结果。
- 不要求如果两个对象根据equals()是不相等的,那么对这两个对象的每一个调用hashCode()必须产生不同的整数结果。 然而,程序员应该注意,对于不相等的对象产生不同的整数结果可能会改善哈希表的性能。
2.重写默认行为
在我们的类中不重写这两种方法时,一切都运行正常。但是,有时应用程序需要更改某些对象的默认行为。
让我们了解为什么需要重写equals和hashCode方法。
2.1. 默认行为
让我们以一个示例来说明,假设您的应用程序有一个Employee对象。让我们创建一个Employee类的最小可能结构:
public class Employee
{
private Integer id;
private String firstname;
private String lastName;
private String department;
//Setters and Getters
}
上面的Employee类具有一些基本属性及其访问器方法。现在考虑一个简单的情况,您需要比较两个Employee对象。两个员工对象都具有相同的id。
public class EqualsTest {
public static void main(String[] args) {
Employee e1 = new Employee();
Employee e2 = new Employee();
e1.setId(100);
e2.setId(100);
System.out.println(e1.equals(e2)); //false
}
}
毫无疑问,上面的方法将打印“false”。
但是在知道两个对象代表同一个员工之后,这是正确的吗?在实时应用程序中,这应该返回true。
2.2. 仅重写equals()
为了实现正确的应用程序行为,我们需要重写equals()方法如下:
public boolean equals(Object o) {
if(o == null)
{
return false;
}
if (o == this)
{
return true;
}
if (getClass() != o.getClass())
{
return false;
}
Employee e = (Employee) o;
return (this.getId() == e.getId());
}
将此方法添加到Employee类中,EqualsTest将开始返回”true”。
那我们完成了吗?还没有。让我们以不同的方式再次测试上面修改后的Employee类。
import java.util.HashSet;
import java.util.Set;
public class EqualsTest
{
public static void main(String[] args)
{
Employee e1 = new Employee();
Employee e2 = new Employee();
e1.setId(100);
e2.setId(100);
//Prints 'true'
System.out.println(e1.equals(e2));
Set<Employee> employees = new HashSet<Employee>();
employees.add(e1);
employees.add(e2);
System.out.println(employees); //Prints two objects
}
}
上面的示例在第二个打印语句中打印两个对象。
如果两个员工对象是相等的,在存储唯一对象的Set中,HashSet内部必须只有一个实例,因为两个对象引用同一个员工。我们漏掉了什么吗?
2.3. 重写hashCode()也是必要的
我们漏掉了第二个重要的方法hashCode()。根据Java文档的说法,如果我们重写了equals(),那么我们必须重写hashCode()。因此,让我们在Employee类中添加另一个方法。
@Override
public int hashCode()
{
final int PRIME = 31;
int result = 1;
result = PRIME * result + getId();
return result;
}
一旦在Employee类中添加了上面的方法,第二个语句将只在第二个语句中打印一个对象,并因此验证了e1和e2的真正相等性。
3.EqualsBuilder和HashCodeBuilder
Apache commons提供了两个出色的实用程序类HashCodeBuilder和EqualsBuilder,用于生成哈希码和equals方法。
我们可以通过以下方式使用这些类。
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class Employee
{
private Integer id;
private String firstname;
private String lastName;
private String department;
//Setters and Getters
@Override
public int hashCode()
{
final int PRIME = 31;
return new HashCodeBuilder(getId()%2==0?getId()+1:getId(), PRIME).toHashCode();
}
@Override
public boolean equals(Object o) {
if (o == null)
return false;
if (o == this)
return true;
if (o.getClass() != getClass())
return false;
Employee e = (Employee) o;
return new EqualsBuilder().
append(getId(), e.getId()).
isEquals();
}
}
4. 在Eclipse IDE中生成hashCode()和equals()
大多数编辑器提供常见的源代码模板。例如,Eclipse IDE有一个选项可以生成优秀的hashCode()和equals()的实现。
右键单击Java文件 -> Source -> Generate hashCode() and equals() …
5. 遵循的最佳实践
- 始终使用相同的字段生成hashCode()和equals()。在我们的案例中,我们使用了员工id。
- equals()必须保持一致(如果对象没有被修改,那么它必须始终返回相同的值)。
- 每当a.equals(b),那么a.hashCode()必须与b.hashCode()相同。
- 如果我们重写了一个方法,那么我们应该重写另一个方法。
6.在JPA Entity声明时要特别注意
如果您正在处理ORM,请确保始终使用getter,而不要在hashCode()和equals()中使用字段引用。因为在ORM中,字段有时会延迟加载,直到我们调用它们的getter方法。
例如,在我们的Employee类中,如果我们使用e1.id == e2.id,那么id字段很可能是延迟加载的。因此,在这种情况下,方法内的id字段可能为零或null,从而导致不正确的行为。
但是如果使用e1.getId() == e2.getId(),我们可以确保即使字段是延迟加载的,调用字段的getter将首先填充字段。