Java equals 和 hashCode 的这几个问题可以说明白吗?

308 阅读6分钟

前言 

昨日有看到一个有关equals和hashcode问题。基础面试经常会碰到与之相关的问题,这不是一个复杂的问题,但很多朋友都 苦于说明他们二者的关系和约束,于是写本文做单独说明,本篇文章将循序渐进(通过举例,让记忆与理解更轻松)说明这些让 你有些苦恼的问题,Let's go 

面试问题 

1. Java里面有了==运算符,为什么还需要equals ? 

==比较的是对象地址,equals 比较的是对象值 

先来看一看object类中equals方法:

public boolean equals(Object obj) { 
  return (this == obj); 
}

我们看到equals方法同样是通过==比较对象地址,并没有帮我们比较值。Java世界中Object 绝对是"老祖宗"的存在,==号 我们没办法改变或重写。但equals是方法,这就给了我们重写equals方法的可能,让我们实现其对值的比较: 

@Override 
public boolean equals(Object obj) 

{ 

新买的电脑,每个电脑都有唯-的序列号, 通常情况下,两个一模一样的电脑放在面前,你会说由于序列号不一样,这两个电脑 不一样吗? 

如果我们要说两个电脑一样,通常是比较其「品牌/尺寸/配置」(值),比如这样: 

@Override
public boolean equals(Object obj) {
  return 品牌相等 && 尺存相等 && 配置相等
}

当遇到如上场景时,我们就需要重写equals方法。这就解释了Java世界为什么有了=还equals这个问题了。 

2. equals相等和hashcode相等问题 

关于二者,你经常会碰到下面的两个问题: 

●两个对象equals相等,那他们hashCode相等吗? 

●两个对象hashCode相等, 那他们equals相等吗? 

为了说明上面两个问题的结论,这里举一个不太恰当的例子, 只为方便记忆,我们将equals比作一个单词的拼写; hashCode 比 作一个单词的发音,在相同语境下: 

sea/ sea「大海」,两个单词拼写一样,所以 equals相等,他们读音/si:/也一样,所以hashCode就相等,这就回答了第一个问题:

两个对象equals相等,那他们hashCode一定也相等

sea/see「大海/看」 ,两个单词的读音/si:/一样,显然单词是不一样的,这就回答了第二个问题:

两个对象hashCode相等,那他们equals不一定相等

查看Object类的hashCode方法: 

public native int hashCode(); 

继续查看该方法的注释,明确写明关于该方法的约束

其实在这个结果的背后,还有的是关于重写equals方法的约束 

3.重写equals有哪些约束? 

关于重写equals方法的约束,同样在该方法的注释中写的很清楚了,我在这里再说明一下: 

赤橙红绿青蓝紫,七彩以色列;哆来咪发唆拉西,一曲安哥拉, 这些规则不是用来背诵的,只是在你需要重写equals 方法时,打 开JDK查看该方法,按照准则重写就好 

4.什么时候需要我们重写hashCode? 

为了比较值,我们重写equals方法,那什么时候又需要重写hashCode方法呢? 

通常只要我们重写equals方法就要重写hashCode方法

为什么会有这样的约束呢?按照上面讲的原则,两个对象equals 相等, 那他们的hashCode-定也相等。 如果我们只重写 equals方法而不重写hashCode方法,看看会发生什么,举个例子来看: 定义学生类,并通过IDE只帮我们生成equals方法:

public class Student {

 private String name;

 private int age;

 @override
public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null II getClass() != o.getClass() returm false;
 Student student= (Student) o;
 return age = student.age &&
     objects.equals(name, student.name);
  }
}

编写测试代码: 

Student student1 = new Student(); 
student1.setName("日拱-兵"); 
student1.setAge(18); 

Student student2 = new Student(); 
student2.setName("日拱-兵"); 
student2.setAge(18); 

System.out.printn"student1.equals(student)的结果是:” + student1.equals(student2)); 

Set<Student> students = new HashSet<Student>(); 
students.add(student1); 
students.add(student2); 
System.out.println"Student Set集合长度是: " + students.size0); 

Map<Student, java.lang. String> map = new HashMap<Student, java.lang String>(); 
map.putstudent1, "student1"); 
map.putstudent2, "student2"); 
System.out.println"Student Map集合长度是:” + map.keySet).size0); 

查看运行结果: 

student1.equals(student2)的结果是: true 
Student Set集合长度是: 2 
Student Map集合长度是: 2 

很显然,按照集合Set和Map加入元素的标准来看, student1和student2是两个对象,因为在调用他们的put (Set add方法 的背后也是HashMap的put)方法时,会先判断hash值是否相等,这个小伙伴们打开JDK自行查看吧 

所以我们继续重写Student类的hashCode方法: 

@Override 
public int hashCode!() { 
 returm objects.hash(name, age); 
}

重新运行上面的测试,查看结果: 

student1.equals(student2)的结果是: true 
Student Set集合长度是: 1 
Student Map集合长度是: 1 

得到我们预期的结果,这也就是为什么通常我们重写equals方法为什么最好也重写hashCode方法的原因 

●如果你在使用Lombok,不知道你是否注意到Lombok只有一个@EqualsAndHashCode 注解,而没有拆分成@Equals 和@HashCode两个注解,想了解更多Lombok的内容,也可以查看我之前写的文章Lomok使用详解 

●另外通过IDE快捷键生成重写方法时,你也会看到这两个方法放在一起,而不是像getter和setter那样分开 

以上两点都是隐形的规范约束,希望大家也严格遵守这个规范,以防带来不必要的麻烦,记忆的方式有多样,如果记不住这个文字约束,脑海中记住上面的图你也就懂了 

5.重写hashCode为什么总有31这个数字? 

细心的朋友可能注意到,我上面重写hashCode的方法很简答,就是用了Objects.hash方法,进去查看里面的方法: 

public static int hashCode(Object al[) { 
 if(a== null) 
   return 0; 

int result=1; 

for (Object element : a) 
   result=31 * result + (element == null?! 0: element.hashCode0); 

 return result; 
}

这里通过31来计算对象hash值 在如何妙用Spring数据绑定?文章末尾提到的在HandlerMethodArgumentResolverComposite类中有这样一个成员变量: 

private final Map<MethodParameter, HandlerMethodArgumentResolver argumentResolverCache= 
      new ConcurrentHashMap<MethodParameter, HandlerMethodArgumentResolver>(256) 

Map的key是MethodParameter , 根据我们上面的分析,这个类一定也会重写equals 和hashCode方法,进去查看发现,hashCode的计算也用到了31这个数字 

@Override 
public boolean equals(Object other) { 
  if (this == other) { 
     return true; 
}
if (!(other instanceof MethodParameter)) { 
     return false;
MethodParameter otherParam = (MethodParameter) other; 
return (this.parameterlndex == otherParam.parameterlndex && getMember).equals(otherParam.getMember); 
}

@Override 
public int hashCode() { 
  returm (getMember().hashCode() * 31 + this parameterIndex); 
}

为什么计算hash值要用到31这个数字呢?我在网上看到一篇不错的文章,分享给大家,作为科普,可以简单查看一下: 

String hashCode方法为什么选择数字31作为乘子 

总结 

如果还对equals和hashCode关系及约束含混,我们只需要按照上述步骤逐步回忆即可,更好的是直接查看JDK源码;另外拿 出实际的例子来反推验证是非常好的办法。如果你还有相关疑问,也可以留言探讨。

-END-

如果看到这里,说明你喜欢这篇文章,请转发、点赞。