一:Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法
1. Object源码理解:
对象在不重写的情况下使用的是Object的equals方法和hashcode方法,从Object类的源码我们知道,默认的equals 判断的是两个对象的引用指向的是不是同一个对象;而hashcode也是根据对象地址生成的一个整数值(本地方法,返回对象的32位jvm内存地址)
2. 需要重写equals()与hashCode的场景
不同的对象(即通过new创建的对象),其引用值肯定不相等,即在内存中的不同的地址单元中。但是有些时候,我们比较两个对象时,如果他们的内容一样,那么我们就认为这两个对象是相等的,或者按照业务需要根据某些维度或字段来判断对象是否相等,则此时需要我们重写对象的equals()与hashCode()方法。
eg: 要求:当学生的姓名、年龄、性别相等时,认为学生对象是相等的,不一定需要对象地址完全相同
package jianlejun.study;
public class Student {
private String name;// 姓名
private String sex;// 性别
private String age;// 年龄
private float weight;// 体重
private String addr;// 地址
// 重写hashcode方法
@Override
public int hashCode() {
int result = name.hashCode();
result = 17 * result + sex.hashCode();
result = 17 * result + age.hashCode();
return result;
}
// 重写equals方法
@Override
public boolean equals(Object obj) {
if(!(obj instanceof Student)) {
// instanceof 已经处理了obj = null的情况
return false;
}
Student stuObj = (Student) obj;
// 地址相等
if (this == stuObj) {
return true;
}
// 如果两个对象姓名、年龄、性别相等,我们认为两个对象相等
if (stuObj.name.equals(this.name) && stuObj.sex.equals(this.sex) && stuObj.age.equals(this.age)) {
return true;
} else {
return false;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public float getWeight() {
return weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
}
现在我们写个例子测试下结果:
public static void main(String[] args) {
Student s1 =new Student();
s1.setAddr("1111");
s1.setAge("20");
s1.setName("allan");
s1.setSex("male");
s1.setWeight(60f);
Student s2 =new Student();
s2.setAddr("222");
s2.setAge("20");
s2.setName("allan");
s2.setSex("male");
s2.setWeight(70f);
if(s1.equals(s2)) {
System.out.println("s1==s2");
}else {
System.out.println("s1 != s2");
}
}
3. 问答:
1)equals()方法和hashCode()方法是什么?
- equals()和hashCode()都是是Java中万物之源Object类中的方法;
- equals方法用于比较两个对象是否相同,Object类中equals方法的实现是比较引用地址来判断的对象是否是同一个对象,通过覆盖该方法可以实现自定义的判断规则;
- hashCode是jdk根据对象的地址或者字符串或者数字计算该对象的哈希码值的方法。
2)为什么要重写equals()方法?
- Object类中equals方法比较的是两个对象的引用地址,只有对象的引用地址指向同一个地址时,才认为这两个地址是相等的,否则这两个对象就不想等。
- 如果有两个对象,他们的属性是相同的,但是地址不同,这样使用equals()比较得出的结果是不相等的,而我们需要的是这两个对象相等,因此默认的equals()方法是不符合我们的要求的,这个时候我们就需要对equals()方法进行重写以满足我们的预期结果。
- 在java的集合框架中需要用到equals()方法进行查找对象,如果集合中存放的是自定义类型,并且没有重写equals()方法,则会调用Object父类中的equals()方法按照地址比较,往往会出现错误的结果,此时我们应该根据业务需求重写equals()方法。
3)为什么要重写hashCode()方法?
- hashCode()方法用于散列数据的快速存储,HashSet/HashMap/Hashtable类存储数据时都是根据存储对象的hashcode值来进行分类存储的,一般先根据hashcode值在集合中进行分类,在根据equals()方法判断对象是否相同。
- HashMap对象是根据其Key的hashCode来获取对应的Value。
- 生成一个好的hashCode值能提高HashSet查找的性能,差的hashCode值不但不能提高性能,甚至可能造成错误。比如hashCode方法中返回常量,会让,HashSet的查找效率退化为List集合的查找效率;hashCode方法中返回随机数,会让查找结果变的不可预测。
- 好的hashCode生成方式是让对象中的关键属性与质数相乘,并将积相加获取。
4)为什么java中在重写equals()方法后必须对hashCode()方法进行重写?
- 为了维护hashCode()方法的equals协定,该协定指出:如果根据 equals()方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode方法都必须生成相同的整数结果;而两个hashCode()返回的结果相等,两个对象的equals()方法不一定相等。
- HashMap对象是根据其Key的hashCode来获取对应的Value。
- 在重写父类的equals()方法时,也重写hashcode()方法,使相等的两个对象获取的HashCode值也相等,这样当此对象做Map类中的Key时,两个equals为true的对象其获取的value都是同一个,比较符合实际。
5)hashcode()与equals()之间的关系?
equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。(可能由于哈希码在生成时产生冲突造成的)
反过来:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
6)Java中==号与equals()方法的区别
==号在比较基本数据类型时比较的是值,而用==号比较两个对象时比较的是两个对象的地址值。
Object类中equals()方法底层依赖的是==号,那么,在所有没有重写equals()方法的类中,调用equals()方法其实和使用==号的效果一样,也是比较的地址值,然而,Java提供的所有类中,绝大多数类都重写了equals()方法,重写后的equals()方法一般都是比较两个对象的值,此时==依然比较的是引用地址。
7)使用hash算法的目的?
hash算法对于查找元素提供了很高的效率
如果想查找一个集合中是否包含有某个对象,大概的程序代码怎样写呢?
通常是逐一取出每个元素与要查找的对象进行比较,当发现某个元素与要查找的对象进行equals方法比较的结果相等时,则停止继续查找并返回肯定的信息,否则,返回否定的信息,如果一个集合中有很多个元素,比如有一万个元素,并且没有包含要查找的对象时,则意味着你的程序需要从集合中取出一万个元素进行逐一比较才能得到结论。
有人发明了一种哈希算法来提高从集合中查找元素的效率,这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组(使用不同的hash函数来计算的),每组分别对应某个存储区域,根据一个对象的哈希吗就可以确定该对象应该存储在哪个区域HashSet就是采用哈希算法存取对象的集合,它内部采用对某个数字n进行取余(这种的hash函数是最简单的)的方式对哈希码进行分组和划分对象的存储区域;Object类中定义了一个hashCode()方法来返回每个Java对象的哈希码,当从HashSet集合中查找某个对象时,Java系统首先调用对象的hashCode()方法获得该对象的哈希码表,然后根据哈希吗找到相应的存储区域,最后取得该存储区域内的每个元素与该对象进行equals方法比较;这样就不用遍历集合中的所有元素就可以得到结论,可见,HashSet集合具有很好的对象检索性能,但是,HashSet集合存储对象的效率相对要低些,因为向HashSet集合中添加一个对象时,要先计算出对象的哈希码和根据这个哈希码确定对象在集合中的存放位置为了保证一个类的实例对象能在HashSet正常存储,要求这个类的两个实例对象用equals()方法比较的结果相等时,他们的哈希码也必须相等;也就是说,如果obj1.equals(obj2)的结果为true,那么以下表达式的结果也要为true:obj1.hashCode() == obj2.hashCode() ;
换句话说:当我们重写一个对象的equals方法,就必须重写他的hashCode方法,不过不重写他的hashCode方法的话,Object对象中的hashCode方法始终返回的是一个对象的hash地址,而这个地址是永远不相等的。所以这时候即使是重写了equals方法,也不会有特定的效果的,因为hashCode方法如果都不想等的话,就不会调用equals方法进行比较了,所以没有意义了。
8)hashCode造成的内存泄露的问题:
package com.weijia.demo;
import java.util.HashSet;
public class Demo {
public static void main(String[] args){
HashSet<RectObject> set = new HashSet<RectObject>();
RectObject r1 = new RectObject(3,3);
RectObject r2 = new RectObject(5,5);
RectObject r3 = new RectObject(3,3);
set.add(r1);
set.add(r2);
set.add(r3);
r3.y = 7;
System.out.println("删除前的大小size:"+set.size());
set.remove(r3);
System.out.println("删除后的大小size:"+set.size());
}
}
删除前的大小size:3
删除后的大小size:3
发现一个问题了,而且是个大问题呀,我们调用了remove删除r3对象,以为删除了r3,但事实上并没有删除,这就叫做内存泄露,就是不用的对象但是他还在内存中。所以我们多次这样操作之后,内存就爆了。可以查看HashSet的remove的源码,实则调用了HashMap的remove方法
我们看到,在调用remove方法的时候,会先使用对象的hashCode值去找到这个对象,然后进行删除,这种问题就是因为我们在修改了r3对象的y属性的值,又因为RectObject对象的hashCode方法中有y值参与运算,所以r3对象的hashCode就发生改变了,所以remove方法中并没有找到r3了,所以删除失败。即r3的hashCode变了,但是他存储的位置没有更新,仍然在原来的位置上,所以当我们用他的新的hashCode去找肯定是找不到了。
其实上面的方法实现很简单的:如下图:
很简单的一个线性的hash表,使用的hash函数是mod,源码如下:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
这个其实就是mod运算,只是这种运算比%运算要高效。
1,2,3,4,5表示是mod的结果,每个元素对应的是一个链表结构,所以说想删除一个Entry<K,V>的话,首先得到hashCode,从而获取到链表的头结点,然后再遍历这个链表,如果hashCode和equals相等就删除这个元素。
上面的这个内存泄露告诉我一个信息:如果我们将对象的属性值参与了hashCode的运算中,在进行删除的时候,就不能对其属性值进行修改,否则会出现严重的问题。
其实我们也可以看一下8种基本数据类型对应的对象类型和String类型的hashCode方法和equals方法。
其中8中基本类型的hashCode很简单就是直接返回他们的数值大小,String对象是通过一个复杂的计算方式,但是这种计算方式能够保证,如果这个字符串的值相等的话,他们的hashCode就是相等的。8种基本类型的equals方法就是直接比较数值,String类型的equals方法是比较字符串的值的。