当想要往类似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中存取对象时会发生什么呢?
如果我们在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"。
如果只重写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最基本的结构是数组 + 链表,即相同索引位置的元素可能会在一条链表上。即时两者的索引相同,也只能说它们处于同一条链上,但并不能证明两者就是相同的。
如果只重写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了。
如果不重写hashCode()
和equals()
方法,那么自定义类默认使用的就是它的超类Object中的hashCode()
和equals()
。hashCode()
的定义和描述如下:
这里使用的是native修饰的本地方法hashCode()
来直接获取哈希值,它们表示的是对象在内存中的地址。
equals()
的定义和描述如下:
从中可以看到,这里执行的是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也相同,那么证明就是相同的。