写点什么

面试高频考点:hashCode 与 equals

作者:xcbeyond
  • 2021 年 12 月 25 日
  • 本文字数:3934 字

    阅读完需:约 13 分钟

先来看阿里巴巴 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 值一定相同

  • 若两个对象的 hashCode 值相同,它们也不一定相同


所以,如果不重写 hashCode 方法,则会发生两个相同的对象出现在 HashSet 集合中,两个相同的 key 出现在 Map 中,这是不被允许的,综上所述,在日常的开发中,只要重写了 equals 方法,就必须重写 hashCode。

用户头像

xcbeyond

关注

🚩InfoQ写作平台签约作者 2019.06.20 加入

专注于技术输出、分享。 博客:https://xcbeyond.cn 公众号:程序猿技术大咖

评论

发布
暂无评论
面试高频考点:hashCode与equals