equals()
和hashCode()
是java.lang.Object
类中定义的两个方法,这两个方法均用在对象比较的场景,即判断两个对象是否相等。在某些场景下,我们需要将我们自定义类的比较逻辑按照我们的使用场景来修改,就可以通过覆写(Overide
)这两个方法来达到这个目的。下面是Object
类中对这两个方法的定义:1
2
3
4
5public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
1 equals()方法
equals()
方法的作用是判断所传入的对象是否跟当前对象相同,JDK对这个方法的默认实现就是比较两个对象的内存地址,只有当这两个对象的内存地址一样才认为它们相同。
2 hashCode()方法
hashCode()
方法的作用是为对象生成一个int
类型的哈希码(hashcode
),hashCode()
方法主要用于配合Java中基于散列的集合一起工作,比如HashMap
、HashTable
以及HashSet
。
3 equals() & hashCode()
如果我们需要通过自定义的对比逻辑来实现两个对象的比较机制,我们通常需要覆写equals()
。但是必须注意:如果我们覆写了equals()
,那么必须同时覆写hashCode()
方法。因为如果仅仅覆写equals()
,我们的对比机制可能在某些业务能正常工作,但是在结合散列集合(如HashMap
)工作的时候,将不能正确按照我们的预期工作!
- 如果两个对象相等,那它们的
hashCode()
返回值一定相同- 如果两个对象的
hashCode()
返回值相同,这两个对象不一定相等
下面我们来实践一下:
场景预设:我们定义一个Book
类,我们自定义的对比机制为:当且仅当两本书的id
和name
都一样的时候,我们认为它们一样(相等);否则不一样。
3.1 没有覆写equals() & hashCode()的情况
Book
类的定义代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public class Book {
private int id;
private String name;
public Book(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试代码1如下:1
2
3
4
5
6
7
8
9
10
11
12private static void test1() {
Book book1 = new Book(1, "Effective Java");
Book book2 = new Book(1, "Effective Java");
System.out.println("book1.equals(book2): " + book1.equals(book2));
System.out.println("book1.hashCode(): " + book1.hashCode());
System.out.println("book2.hashCode(): " + book2.hashCode());
}
// 测试结果
book1.equals(book2): false
book1.hashCode(): 1590550415
book2.hashCode(): 1058025095
根据我们的场景预设,只要id
和name
相同我们就认为是同一本书,所以我们预期equals()
对比的结果应该是相同的。但是结果并相同,这是因为我们在没有覆写equals()
之前,其默认实现是对比两个对象的地址。我们测试代码中,是分别new
了两个Book
对象,所以它们地址肯定不一样,所以对比结果为false
。
3.2 仅覆写equals()的情况
现在我们在Book
类中覆写equals()
方法,自定义对比机制:1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Book)) {
return false;
}
return this.getId() == ((Book) obj).getId() && this.getName().equals(((Book) obj).getName());
}
我们此时再运行3.1中的test1()
测试代码,结果如下:1
2
3book1.equals(book2): true
book1.hashCode(): 1590550415
book2.hashCode(): 1058025095
现在看来,只要书本的id
和name
相同,我们自定义的对比机制已经能正确判断它们是同一本书;但是,由于我们仅覆写了equals(),没有覆写hashCode()
,那么我们这个比较机制在配合HashMap
、HashTable
以及HashSet
这些散列集合进行使用的时候,将不能正确对比。
测试代码2如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19private static void test2() {
Book book1 = new Book(1, "Effective Java");
Book book2 = new Book(1, "Effective Java");
System.out.println("book1.equals(book2): " + book1.equals(book2));
System.out.println("book1.hashCode(): " + book1.hashCode());
System.out.println("book2.hashCode(): " + book2.hashCode());
// map——维护书本与库存量的关系
Map<Book, Integer> bookStock = new HashMap<>();
// 设置id为1,书名为"Effective Java"的这本书的库存为10
bookStock.put(book1, 10);
// 查询id为1,书名为"Effective Java"的这本书的库存
System.out.println("Book[id: 1, name: Effective Java]: " + bookStock.get(book2));
}
// 测试结果
book1.equals(book2): true
book1.hashCode(): 1590550415
book2.hashCode(): 1058025095
Book[id: 1, name: Effective Java]: null
查询结果为null
,说明这本书(id=1
&&name=Effective Java
)在库存中不存在。可是我们明明已经将这本书(id=1
&&name=Effective Java
)的库存设置成10
并更新到库存里了!哪里出了问题?
前面提到,任何时候覆写equals()
,必须同时覆写hashCode()
方法,否则在结合散列集合将无法正确工作!
这是因为,散列集合在添加、查找的时候都用到了
hashCode()
方法。比如我们测试代码中的HashMap
,在put
或者get
的时候,都会先将Key
对象的hashCode()
返回值进行计算,得到一个hash
值,根据这个值去定位Value
的位置。从上面的测试结果可知,虽然是同一本书,但是它们的hashCode()
返回值却不同。这就导致HashMap
认为book1
和book2
是两个不同的Key
,所以我们在put(book1, 10)
却get(book2)
的时候肯定找不到这本书。
3.3 同时覆写equals() & hashCode()的情况
根据上面的结论,如果我们覆写了equals()
就一定要同时覆写hashCode()
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Book)) {
return false;
}
return this.getId() == ((Book) obj).getId() && this.getName().equals(((Book) obj).getName());
}
public int hashCode() {
// 为了简单演示,我们这里将每本书的hashCode返回值设置成书本的id(保证唯一性)
return this.getId();
}
然后我们再运行3.2中的test2()
测试代码,结果如下:1
2
3
4book1.equals(book2): true
book1.hashCode(): 1
book2.hashCode(): 1
Book[id: 1, name: Effective Java]: 10
运行结果显示,同时覆写equals()
和hashCode()
之后,程序已经如我们的预期正确运行。虽然book1
和book2
是两个不同的对象(对象地址不一样),但是我们通过覆写equals()
和hashCode()
来定制我们的对比机制,达到我们自定义的对象对比逻辑,满足我们一些使用场景。
4 总结
如果我们在使用自定义类的时候,想自定义对象的对比机制来达到某些需求场景的要求,例如上面的例子:如果书本的编号(id
)和书本的名字(name
)都相同,则认为它们是同一本书。我们可以通过同时覆写equals()
和hashCode()
来达到全面的效果,而不是局部起作用(如3.2章节中仅覆写equals()
的情况)。
记住:任何时候,只要覆写了equals()
就一定要同时覆写hashCode()
!
——————–【参考文章】——————–