为什么重写了equals方法,就必须重写hashCode
先来看阿里巴巴Java开发手册中的一段话:
【强制】关于 hashCode 和 equals 的处理,遵循如下规则:1) 只要重写 equals,就必须重写 hashCode。2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的 对象必须重写这两个方法。3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals。说明:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象 作为 key 来使用。
它要求我们若是重写equals方法则必须强制重写hashCode,这是为何呢?
1equals和hashCode方法
我们先来了解一下这两个方法,它们都来自Object类,说明每一个类中都会有这么两个方法,那它俩的作用是什么呢?
首先是equals方法,它是用来比较两个对象是否相等。对于equals方法的使用,得分情况讨论,若是子类重写了equals方法,则将按重写的规则进行比较,比如:
public static void main(String[] args) {
String s = "hello";
String str2 = "world";
boolean result = s.equals(str2);
System.out.println(result);
}
来看看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类调用equals方法比较的将是字符串的内容是否相等。又如:
public static void main(String[] args) {
Integer a = 500;
Integer b = 600;
boolean result = a.equals(b);
System.out.println(result);
}
观察Integer类的实现:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
它比较的仍然是值,然而若是没有重写equals方法:
@AllArgsConstructor
static class User {
private String name;
private Integer age;
}
public static void main(String[] args) {
User user = new User("zs", 20);
User user2 = new User("zs", 20);
boolean result = user.equals(user2);
System.out.println(result);
}
即使两个对象中的值是一样的,它也是不相等的,因为它执行的是Object类的equals方法:
public boolean equals(Object obj) {
return (this == obj);
}
我们知道,对于引用类型,==
比较的是两个对象的地址值,所以结果为false,若是想让两个内容相同的对象在equals后得到true,则需重写equals方法:
@AllArgsConstructor
static class User {
private String name;
private Integer age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) && Objects.equals(age, user.age);
}
}
再来聊一聊hashCode方法,它是一个本地方法,用来返回对象的hash码值,通常情况下,我们都不会使用到这个方法,只有Object类的toString方法使用到了它:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
2为什么只要重写了equals方法,就必须重写hashCode
了解两个方法的作用后,我们来解决本篇文章的要点,为什么只要重写了equals方法,就必须重写hashCode呢?这是针对一些使用到了hashCode方法的集合而言的,比如HashMap、HashSet等,先来看一个现象:
public static void main(String[] args) {
Map<Object, Object> map = new HashMap<>();
String s1 = new String("key");
String s2 = new String("key");
map.put(s1, 1);
map.put(s2, 2);
map.forEach((k, v) -> {
System.out.println(k + "--" + v);
});
}
这段程序的输出结果是:key--2
,原因是HashMap中的key不能重复,当有重复时,后面的数据会覆盖原值,所以HashMap中只有一个数据,那再来看下面一段程序:
@AllArgsConstructor
@ToString
static class User {
private String name;
private Integer age;
}
public static void main(String[] args) {
Map<Object, Object> map = new HashMap<>();
User user = new User("zs", 20);
User user2 = new User("zs", 20);
map.put(user, 1);
map.put(user2, 2);
map.forEach((k, v) -> {
System.out.println(k + "--" + v);
});
}
它的结果应该是什么呢?是不是和刚才一样,HashMap中也只有一条数据呢?可运行结果却是这样的:
EqualsAndHashCodeTest.User(name=zs, age=20)--1
EqualsAndHashCodeTest.User(name=zs, age=20)--2
这是为什么呢?这是因为HashMap认为这两个对象并不相同,那我们就重写equals方法:
@AllArgsConstructor
@ToString
static class User {
private String name;
private Integer age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) && Objects.equals(age, user.age);
}
}
public static void main(String[] args) {
Map<Object, Object> map = new HashMap<>();
User user = new User("zs", 20);
User user2 = new User("zs", 20);
System.out.println(user.equals(user2));
map.put(user, 1);
map.put(user2, 2);
map.forEach((k, v) -> {
System.out.println(k + "--" + v);
});
}
运行结果:
true
EqualsAndHashCodeTest.User(name=zs, age=20)--1
EqualsAndHashCodeTest.User(name=zs, age=20)--2
两个对象判断是相同的,但HashMap中仍然存放了两条数据,说明HashMap仍然认为这是两个不同的对象。这其实涉及到HashMap底层的原理,查看HashMap的put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在存入数据之前,HashMap先对key调用了hash方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
该方法会调用key的hashCode方法并做右移、异或等操作,得到key的hash值,并使用该hash值计算得到数据的插入位置,如果当前位置没有元素,则直接插入,如下图所示:
既然两个对象求得的hash值不一样,那么就会得到不同的插入位置,由此导致HashMap最终存入了两条数据。
接下来我们重写User对象的hashCode和equals方法:
@AllArgsConstructor
@ToString
static class User {
private String name;
private Integer age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) && Objects.equals(age, user.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
那么此时两个对象计算得到的hash值就会相同:当通过hash计算得到相同的插入位置后,user2便会发现原位置上已经有数据了,此时将触发equals方法,对两个对象的内容进行比较,若相同,则认为是同一个对象,再用新值覆盖旧值,所以,我们也必须重写equals方法,否则,HashMap始终会认为两个new 出来的对象是不相同的,因为它俩的地址值不可能一样。
由于String类重写了hashCode和equals方法,所以,我们可以放心大胆地使用String类型作为HashMap的key。
在HashSet中,同样会出现类似的问题:
@AllArgsConstructor
@ToString
static class User {
private String name;
private Integer age;
}
public static void main(String[] args) {
Set<Object> set = new HashSet<>();
User user = new User("zs", 20);
User user2 = new User("zs", 20);
set.add(user);
set.add(user2);
set.forEach(System.out::println);
}
对于内容相同的两个对象,若是没有重写hashCode和equals方法,则HashSet并不会认为它俩重复,所以会将这两个User对象都存进去。
3总结
hashCode的本质是帮助HashMap和HashSet集合加快插入的效率,当插入一个数据时,通过hashCode能够快速地计算插入位置,就不需要从头到尾地使用equlas方法进行比较,但为了不产生问题,我们需要遵循以下的规则:
两个相同的对象,其hashCode值一定相同 若两个对象的hashCode值相同,它们也不一定相同
所以,如果不重写hashCode方法,则会发生两个相同的对象出现在HashSet集合中,两个相同的key出现在Map中,这是不被允许的,综上所述,在日常的开发中,只要重写了equals方法,就必须重写hashCode。
扫一扫,关注我们