Java中自定义类为什么一定要重写HashCode和equals方法?

文章目录

      • 1. 引入
      • 2. 两者都不重写
      • 3. 只重写hashCode方法
      • 4. 只重写equals方法
      • 5. 原理分析


1. 引入

当想要往类似HashMap的Map接口的实现类对象中存放Java中的包装类对象,例如String、Integer等时,我们可以直接存取,例如:

@Test
public void testString(){
    HashMap<Integer, String> map = new HashMap<>();
    map.put(10, "Kobe");
    map.put(23, "James");

    System.out.println(map.get(10));  // Kobe


    Integer a = 10;
    Integer b = 10;
    System.out.println(map.get(a) + "---" + map.get(b)); // Kobe---Kobe

}

而如果要存放的是自定义的类对象,我们需要重写hashCode()equals(),然后才能在Map中正确的存放和获取对象,例如自定义Book类,类中只有Integer类型的price,并且重写了hashCode()equals(),如下所示:

public class Book{
    private Integer price;

    public Book(Integer price) {
        this.price = price;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(price, book.price);
    }

    @Override
    public int hashCode() {
        return Objects.hash(price);
    }

    @Override
    public String toString() {
        return "Book{" +
                "price=" + price +
                '}';
    }
}

然后使用Map存取Book实例对象一切正常,例如:

@Test
public void test(){
    HashMap<Book, String> map = new HashMap<>();

    Book b1 = new Book(10);
    Book b2 = new Book(10);

    System.out.println("b1 hashcode is: " + b1.hashCode() + " and b2 hashcode is: " + b2.hashCode());
    // b1 hashcode is: 41 and b2 hashcode is: 41


    map.put(b1, "AI");
    System.out.println(map.get(b1));  // AI
    String name = map.get(b2);
    System.out.println(name);  // AI
}

如果我们不重写这两个方法,或者只重写其中的某一个方法,那么在Map中存取对象时会发生什么呢?


2. 两者都不重写

如果我们在Book类中既不重写hashCode(),也不重写equals()。依然使用HashMap来存取对象,例如:

@Test
public void testNoHashcodeNoEquals() {
    HashMap<Book, String> map = new HashMap<>();

    Book b1 = new Book(10);
    Book b2 = new Book(10);

    System.out.println("b1 hashcode is: " + b1.hashCode() + " and b2 hashcode is: " + b2.hashCode());
    // b1 hashcode is: 1265094477 and b2 hashcode is: 2125039532


    map.put(b1, "AI");
    System.out.println(map.get(b1));  // AI
    String name = map.get(b2);
    System.out.println(name);  // null
}

如上代码注释标识的单元测试结果所示,Book对象b1和b2的传参是一样的,但是它们的hashCode不同。这是因为b1和b2是通过两次new得到的对象,那么它们在堆内存中属于两个不同的对象,对应的堆内存地址自然不同。因此,由hashCode标识的内存地址也就不可能相同了。

如果将key=b1, value="AI"的键值对存放在HashMap中,使用b1可以得到"AI",而使用b2得到的是null。这是因为存放b1时,HashMap内部的put()会根据对象的hashCode计算得到它在内部table中的索引,然后将其存放在table对应索引的位置上。由于b1和b2两者的hashCode不同,那么计算得到的索引位置不同,自然使用b2就无法获取到"AI"。


3. 只重写hashCode方法

如果只重写hashCode(),而不重写equals(),执行上述相同的过程会发生什么呢?如下所示:

@Test
public void testNoEquals(){
    HashMap<Book, String> map = new HashMap<>();

    Book b1 = new Book(10);
    Book b2 = new Book(10);

    System.out.println("b1 hashcode is: " + b1.hashCode() + " and b2 hashcode is: " + b2.hashCode());
    // b1 hashcode is: 41 and b2 hashcode is: 41


    map.put(b1, "AI");
    System.out.println(map.get(b1));  // AI

    Set<Map.Entry<Book, String>> entries = map.entrySet();
    for (Map.Entry<Book, String> entry : entries) {
        System.out.println(entry.toString());  // Book{price=10}=AI
    }

    String name = map.get(b2);
    System.out.println(name);  // null
}

从注释标识的输出结果中可以看到,此时b1和b2的hashCode都是41,这就意味着如果将其存放到HashMap中,它们对应的table中的索引应该是相同的。但是,为什么还是无法使用b2获取到"AI"呢?

虽然b1和b2两者指向table中相同的位置,但是根据HashMap的实现源码可知,HashMap最基本的结构是数组 + 链表,即相同索引位置的元素可能会在一条链表上。即时两者的索引相同,也只能说它们处于同一条链上,但并不能证明两者就是相同的。


4. 只重写equals方法

如果只重写equals方法呢?如下所示:

@Test
public void testNoHashCode(){
    HashMap<Book, String> map = new HashMap<>();

    Book b1 = new Book(10);
    Book b2 = new Book(10);

    System.out.println("b1 hashcode is: " + b1.hashCode() + " and b2 hashcode is: " + b2.hashCode());
    // b1 hashcode is: 1645995473 and b2 hashcode is: 1463801669

    map.put(b1, "AI");
    System.out.println(map.get(b1));  // AI

    String name = map.get(b2);
    System.out.println(name);  // null
}

由于并没有重写hashCode(),两者的hashCode不同,导致它们在table中的索引位置不同,自然就不同使用b2来获取b1在HashMap中存放的value了。


5. 原理分析

如果不重写hashCode()equals()方法,那么自定义类默认使用的就是它的超类Object中的hashCode()equals()hashCode()的定义和描述如下:


Java中自定义类为什么一定要重写HashCode和equals方法?_第1张图片

这里使用的是native修饰的本地方法hashCode()来直接获取哈希值,它们表示的是对象在内存中的地址。

equals()的定义和描述如下:


Java中自定义类为什么一定要重写HashCode和equals方法?_第2张图片

从中可以看到,这里执行的是this == obj,使用==进行对象的比较,其实就是对象引用的比较,不同时间new的对象不同,对应的对象引用自然也就不同。所以,如果不重写这两个方法,那么使用看起来相同的键是无法获取已有的值的。

而在自定义类中重写的这两个方法如下所示:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Book book = (Book) o;
    return Objects.equals(price, book.price);
}

@Override
public int hashCode() {
    return Objects.hash(price);
}

其中hashCode()只关心price是不是相同,只要值相同,那么调用Objects的hashCode()得到的哈希值就是相同的,它们在table中的索引就是相同的。equals()首先判断两个对象的引用是否相同,如果相同,表示两者就是同样的对象,直接返回true。如果两者的引用不同,继续判断传入的对象是否为null或者两者的类型是否相同,如果不同直接返回false,根本就不同同一类型对象。否则,将传入的对象强转为待比较的对象类型,然后再调用Obejcts的equals()进行比较,如果两者的类型相同,传入的对象不为null,而且它们的price也相同,那么证明就是相同的。

你可能感兴趣的:(Java)