先来看阿里巴巴 Java 开发手册中的一段话:
【强制】关于 hashCode 和 equals 的处理,遵循如下规则:1) 只要重写 equals,就必须重写 hashCode。2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的 对象必须重写这两个方法。3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals。说明:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象 作为 key 来使用。
它要求我们若是重写 equals 方法则必须强制重写 hashCode,这是为何呢?
equals 和 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());
}
复制代码
为什么只要重写了 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 对象都存进去。
总结
hashCode 的本质是帮助 HashMap 和 HashSet 集合加快插入的效率,当插入一个数据时,通过 hashCode 能够快速地计算插入位置,就不需要从头到尾地使用 equlas 方法进行比较,但为了不产生问题,我们需要遵循以下的规则:
所以,如果不重写 hashCode 方法,则会发生两个相同的对象出现在 HashSet 集合中,两个相同的 key 出现在 Map 中,这是不被允许的,综上所述,在日常的开发中,只要重写了 equals 方法,就必须重写 hashCode。
评论