为什么重写equals()方法的时候要重写hashCode()方法,这次总算弄明白了
一、equals()方法
先说说equals()方法。
Java中Object类中的equals()方法,源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到这里直接用’==’来直接比较
我们知道Java有8种基本类型:数值型(byte、short、int、long、float、double)、字符型(char)、布尔型(boolean),对于这8种基本类型的比较,变量存储的就是值,所以比较的就是’值’本身。如下,值相等就是true,不等就是false。
public class ValueCompare {
public static void main(String[] args) {
int a=3;
int b=4;
int c=3;
System.out.println(a==b); //false
System.out.println(a==c); //true
}
}
对于非基本类型,也就是常说的引用数据类型:类、接口、数组,由于变量中存储的是内存地址,并不是’值’本身,所以真正比较的是该变量存储的地址,可想而知,如果声明的时候是2个对象,地址固然不同, 使用==号比较就会是false。
public class ReferenceCompare {
public static void main(String[] args) {
String str1 = new String("123");
String str2 = new String("123");
System.out.println(str1 == str2); //false
}
}
可以看到,上面这种比较方法,和Object类中的equals()方法的具体实现相同,之所以为false,是因为直接比较的是str1和str2指向的地址,也就是说Object中的equals方法是直接比较的地址,因为Object类是所有类的基类,所以调用新创建的类的equals方法,比较的就是两个对象的地址。那么就有人要问了,如果就是想要比较引用类型实际的值是否相等,该如何比较呢?
要解决上面的问题,就是今天要说的equals(),具体的比较由各自去重写,比较具体的值的大小。我们可以看看上面字符串的比较,如果调用String的equals方法的结果。
public class ReferenceCompare {
public static void main(String[] args) {
String str1 = new String("123");
String str2 = new String("123");
System.out.println(str1.equals(str2)); //true
}
}
可以看到比较的结果返回的是true,我们可以看一下String底层调用equals()的源码是怎么比较的。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
可以看到String底层是对其从父类Object类中继承的equals方法进行了重写
重写后的比较逻辑是,
- 先使用==号比较两个对象的内存地址是否一致, 如果一致则直接返回true.
- 如果不一致, 接着再使用instanceof方法判断调用equals方法的对象是否是String类的一个实例, 如果不是则直接返回false
- 如果是String类的实例, 则将该对象先强转成String对象, 接着先比较两个字符串的长度是否相等,不相等直接返回false,
- 相等再继续比较, 将字符串拆成一个一个的字符依次比较, 如果每个对应位置上的字符都相等才返回true, 否则返回false
由此可知, String类通过对Object类的equals方法进行了重写, 使用重写后的equals方法进行比较时, 不管两个字符串的内存地址一不一样, 只要值相等就一定相等
二、只重写equals方法会造成的问题
接着, 我们来看一下只重写equals方法时, 会造成什么问题
首先, 自定义一个Student类, 重写equals方法, 使得该类使用equals方法比较不管内存地址相不相等, 只要姓名,年龄,身份证号都相等就一定相等, 是同一个人
public class Student {
private String name; //姓名
private int age; //年龄
private String idCard; //身份证号
public Student(String name, int age, String idCard) {
this.name = name;
this.age = age;
this.idCard = idCard;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name) &&
Objects.equals(idCard, student.idCard);
}
}
接下来, 看看我们的测试类
public class Test {
public static void main(String[] args) {
Student student1 = new Student("马云",20,"123");
Student student2 = new Student("马云",20,"123");
System.out.println(student1.equals(student2)); //true
System.out.println(student1.hashCode()); //1163157884
System.out.println(student2.hashCode()); //1956725890
HashMap<Student, String> map = new HashMap<>();
map.put(student1, "abc");
map.put(student2, "def");
System.out.println(map.get(student1)); //abc
System.out.println(map.get(student2)); //def
}
}
我们知道hashmap中键是唯一的, 不允许两个相同的键出现.
但从上述代码的打印结果我们可以看出, 在我们认为的是同一个对象的student1和student2, 在HashMap看来不是一个相同的key
发现出现了矛盾???
用equals比较说明对象相同,但是在HashMap中却以不同的对象存储
(没有重写hascode值,两个hascode值,在他看来就是两个对象)。
到底这两个对象相等不相等????
三、hashmap存取value元素的原理
我们想要搞清楚为什么会造成上述矛盾的地方, 就得知道hashmap存取value元素的原理
(1) 向HashMap存储kv数据的时候,会首先调用k对象的hashCode方法计算k的哈希值, 然后再拿该hash值&上自身无符号右移16位的值定位元素在数组中要存储的位置,
我们分析上面那段代码,HashMap的key是我们自己定义的一个类,可以看到,我们没有重写hashCode方法,意思是hashmap在进行存储的时候是调用的Object类中的hashCode()方法。
而Object类中的hashCode()方法底层是一个本地方法只要对象在堆内存中的地址不同, 则调用这个本地方法返回的哈希值就不同, 由此, 两个student对象就被存储到了hashmap中的不同位置
public native int hashCode();
四、hashCode()方法
修改后的Student类
public class Student {
private String name; //姓名
private int age; //年龄
private String idCard; //身份证号
public Student(String name, int age, String idCard) {
this.name = name;
this.age = age;
this.idCard = idCard;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name) &&
Objects.equals(idCard, student.idCard);
}
@Override
public int hashCode() {
return Objects.hash(name, age, idCard);
}
}
再次执行测试类的main方法查看输出结果
public class Test {
public static void main(String[] args) {
Student student1 = new Student("马云",20,"123");
Student student2 = new Student("马云",20,"123");
System.out.println(student1.equals(student2)); //true
System.out.println(student1.hashCode()); //1197105506
System.out.println(student2.hashCode()); //1197105506
HashMap<Student, String> map = new HashMap<>();
map.put(student1, "abc");
map.put(student2, "def");
System.out.println(map.get(student1)); //def
System.out.println(map.get(student2)); //def
}
}
可以看到, 此时相同的两份对象存储到hashmap中就只存储了一份了
五、查看Object类中的hashcode方法, 查看Objects工具类中的hashcode方法
Objects工具类中的hash方法
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
Arrays工具类中的hashCode方法
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
Object类中的hashcode方法底层调用的是本地native方法
public native int hashCode();
六、总结
为什么重写 equals 时必须重写 hashCode 方法?
我们重写equals时,是为了用自身的方式去判断两个自定义对象是否相等,然而如果此时刚好需要我们用自定义的对象去充当hashmap的键值使用时,就会出现我们认为的同一对象,却因为hash值不同而导致hashmap中存了两个对象,从而才需要进行hashcode方法的重写
(注意: HashMap、HashTable、HashSet底层都是采用的这种方式存储值的)