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()!
——————–【参考文章】——————–
