引言
在学习集合中,HashMap/HashSet是非常常用的集合,但是在使用时刻要想着重写equals方法和hashCode方法,所以在这里简单说一说在使用HashMap或者HashSet中为什么必须要重写equals方法和hashCode方法,
在介绍之前先简单介绍一下一种数据结构——哈希表,如果不理解这个数据结构,那么不利于对重写原因的理解的;
哈希表
基本思想: 记录的存储位置与关键字之间存在对应关系,Loc(i)=H(keyi) (注:这是一个哈希函数)
优点: 查找速度极快O(1),查找效率与元素个数n无关
注:略去部分内容,只看本文需要的内容;
哈希表的一种存储方法(处理冲突的方法)叫做:链地址法,下面也围绕这个方法展开的
基本思想:相同哈希地址的记录链成一单链表,m个哈希地址就设m个单链表,然后用用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构;(一定要理解这句话!!!)
就像这样子,每一个哈希地址对应一个单链表,每个单链表上的每个key值对应一个value值;所以看哈希表就像一个数组和多个链表的合体;
先有个大致印象,下面我们看看HashMap的结构
解释一下:哈希地址就类似数组下标,在图中用hash表示,HashMap中的key值就是k,Vaule值就是v;
当我们往HashMap中存储数据时,先通过hash确定到某一条链表上,然后再通过key值在链表中进行比较,如果没有相同的key值的话,就在最后新创建一个节点把Value值v放进去,如果有相等的key值,那么新的v会覆盖之前的v
那么hash值是从哪来的呢?
hash值就是key的hashCode的返回值;
有了hash值后key值比较的过程就是使用equals方法;
能看出来为什么要重写hashCode方法和equals方法了吗?
就是因为每一个对象的hashCode值都不相同,而不同的对象使用equals方法比较的只是内存地址,而内存地址也不相同;
我们想要的结果只是 相同内容的hashCode值相同,相同内容的比较相同!
所以我们要针对我们的对象内容重写hashCode方法和equals方法;
下面通过例子更形象的说明:
实例说明
这里我自定义了一个User类,并没有重写HashCode和equals;
我们先看看有相同内容的对象的HashCode值是否相同:
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class MapTest05 {
public static void main(String[] args) {
Map<User, String> map = new HashMap<>();
// 创建四个User对象
User u1 = new User("张三", 18);
User u2 = new User("李四", 20);
User u3 = new User("王五", 30);
User u4 = new User("王五", 30); // 创建一个和u3内容一样的对象u4
// 先看看hashCode值
System.out.println("u1的hashCode值为:" + u1.hashCode());
System.out.println("u2的hashCode值为:" + u2.hashCode());
System.out.println("u3的hashCode值为:" + u3.hashCode());
System.out.println("u4的hashCode值为:" + u4.hashCode());
}
}
// 用户类
class User {
private String name; // 姓名
private int age; // 年龄
// 默认构造
User() {}
// 有参构造
User(String name, int age) {
this.name = name;
this.age = age;
}
// equals方法未重写
// hashcode方法未重写
}
输出结果:
u1的hashCode值为:2003749087
u2的hashCode值为:1480010240
u3的hashCode值为:81628611
u4的hashCode值为:1828972342
可以看到,每一个对象的hashCode值都不同,尤其注意u3和u4内容相同而hashCode值不同;
接下来把它们放到map中看看会怎样;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapTest05 {
public static void main(String[] args) {
Map<User, String> map = new HashMap<>();
// 创建四个User对象
User u1 = new User("张三", 18);
User u2 = new User("李四", 20);
User u3 = new User("王五", 30);
User u4 = new User("王五", 30); // 创建一个和u3内容一样的对象u4
// 将User作为索引放入map中
map.put(u1, "是个小帅哥");
map.put(u2, "是个大帅哥");
map.put(u3, "是个老帅哥");
map.put(u4, "是个帅哥");
// 输出map
Set<Map.Entry<User, String>> set = map.entrySet();
for (Map.Entry<User, String> i : set) {
System.out.println(i.getKey() + " = " + i.getValue());
}
}
}
// 用户类
class User {
private String name; // 姓名
private int age; // 年龄
// 默认构造
User() {}
// 有参构造
User(String name, int age) {
this.name = name;
this.age = age;
}
// equals方法未重写
// hashcode方法未重写
}
输出结果:
map.User@776ec8df = 是个小帅哥
map.User@41629346 = 是个帅哥
map.User@4eec7777 = 是个大帅哥
map.User@3b07d329 = 是个老帅哥
可以看到,u3和u4内容相同,但是放到HashMap中后u4并没有把u3覆盖,这样子是万万不行的;我们都知道:
哈希表中一个索引key对应的只能是一个value值,而在这里u3和u4的索引按道理来说都相同,但是却对应不同的value值;
这就是因为我们没有重写hashCode,我们认为的索引是u3、u4的内容,而实际情况下的索引是它们不同的hashCode值,所以就会造成这种情况的发生;
如果重写了hashCode和equals方法呢?我们还是先看看重写后hashCode值;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class MapTest05 {
public static void main(String[] args) {
Map<User, String> map = new HashMap<>();
// 创建四个User对象
User u1 = new User("张三", 18);
User u2 = new User("李四", 20);
User u3 = new User("王五", 30);
User u4 = new User("王五", 30); // 创建一个和u3内容一样的对象u4
// 先看看hashCode值
System.out.println("u1的hashCode值为:" + u1.hashCode());
System.out.println("u2的hashCode值为:" + u2.hashCode());
System.out.println("u3的hashCode值为:" + u3.hashCode());
System.out.println("u4的hashCode值为:" + u4.hashCode());
}
}
// 用户类
class User {
private String name; // 姓名
private int age; // 年龄
// 默认构造
User() {}
// 有参构造
User(String name, int age) {
this.name = name;
this.age = age;
}
// 重写equals方法
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return age == user.age && name.equals(user.name);
}
// 重写hashCode方法
public int hashCode() {
return Objects.hash(name, age);
}
}
输出结果
u1的hashCode值为:24022538
u2的hashCode值为:26104872
u3的hashCode值为:29050006
u4的hashCode值为:29050006
和没有重写的差别一眼就能看出来了,尤其是u3和u4,这次因为它们的对象内容是相同的,所以它们的hashCode也是相同的;
那么这样子的话把它们放到HashMap中会怎样?看看下面代码:
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class MapTest05 {
public static void main(String[] args) {
Map<User, String> map = new HashMap<>();
// 创建四个User对象
User u1 = new User("张三", 18);
User u2 = new User("李四", 20);
User u3 = new User("王五", 30);
User u4 = new User("王五", 30); // 创建一个和u3内容一样的对象u4
// 将User作为索引放入map中
map.put(u1, "是个小帅哥");
map.put(u2, "是个大帅哥");
map.put(u3, "是个老帅哥");
map.put(u4, "是个帅哥");
// 输出map
Set<Map.Entry<User, String>> set = map.entrySet();
for (Map.Entry<User, String> i : set) {
System.out.println(i.getKey() + " = " + i.getValue());
}
}
}
// 用户类
class User {
private String name; // 姓名
private int age; // 年龄
// 默认构造
User() {}
// 有参构造
User(String name, int age) {
this.name = name;
this.age = age;
}
// 重写equals方法
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return age == user.age && name.equals(user.name);
}
// 重写hashCode方法
public int hashCode() {
return Objects.hash(name, age);
}
}
输出结果:
map.User@16e8e0a = 是个小帅哥
map.User@18e5428 = 是个大帅哥
map.User@1bb4496 = 是个帅哥
可以看到输出结果变成了三个,而且因为u3和u4的内容相同,所以最后的u4的value覆盖了u3,这就实现了我们的需求:通过索引对象的内容进行比较;
ps:如果我的语言描述有问题的话可以自己试试这几个代码,体会一下我所想要描述的意思,这一块我可能说的有点啰嗦,就有点乱,自己敲代码试试就会明白我说的是什么了;
关于HashSet
其实HashSet和HashMap是一样的,在HashSet底层调用了HashMap,底层代码如图:
所以记住一点就行了:HashMap需要注意的HashSet也需要注意
总结
说了这么多,其实就是想要说一句话:
放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode方法和equals方法;
这也是这篇文章讨论的核心,所以在写代码的时候一定要记住;如果没有看懂的话那就记住这句话就行了!
对了还要注意一点:包装类和String类的hashCode方法和equals方法SUN公司都已经重写过了,所以不需要重写了!!
可以自己写个代码验证一下,这里就不举例了;