您的位置:首页 > 编程语言 > Java开发

深入解析Java中的equals()和hashCode()方法

2017-10-13 22:22 691 查看
  我们知道在Java中所有对象都是继承于Object类的,而equals()和hashCode()是Object类的公共方法,这两个方法是用于同一类中作为比较用的,特别是用于判断往Set这样的容器中放入的对象是否重复。

等号(==):对于基本类型直接比较值是否相等,对于对象实例则比较两者的内存地址是否相等。

equals():默认是由内存地址判断对象是否相等,可根据实际业务需求进行重写。

由java集合的需求来分析hashCode的作用:

  我们都知道java(Collection)中有一类Set容器,该容器中的元素无序而且不可重复。那么应该如何来确保元素的不重复性的,我们很容易可以想到可以利用Object类的equals()方法来进行比较,但是这样子每当加入一个新元素时都要调用一次equals()方法,当元素数量很多时,效率会变得非常低。那么java是如何解决这个问题的呢?

  这里就体现出hashCode的价值所在了。Java采用了哈希表的原理。哈希算法又称为散列算法,是将数据依特定算法直接指定到一个地址上。可以这样简单理解,hashCode方法实际上返回的就是对象存储位置的映像。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就能定位到它应该放置的存储位置。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就表示发生冲突了,散列表对于冲突有具体的解决办法,但最终还会将新元素保存在适当的位置。这样一来,实际调用equals方法的次数就大大降低了,几乎只需要一两次。

  下面看一下对象放入散列集合的流程图:



  由上面流程图可以看出,我们在存储一个对象的时候,首先进行hashCode的比较,当hashCode相等时,再进行equals的比较,通过查阅资料我们对hashCode()方法与equals()方法总结如下:

若重写了equals(Object obj)方法,则有必要重写hashCode()方法。

若两个对象equals()返回true,则hashCode()也必须返回相同的int数。

若两个对象equals()返回false,则hashCode()不一定返回不同的int数。

若两个对象hashCode()返回相同int数,则equals()不一定返回true。

若两个对象hashCode()返回不同int数,则equals()一定返回false。

同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。

那么什么时候需要重写equals()方法和hashCode()方法呢?

  通常来说,我们会根据具体的业务需求重写equals()方法来比较不同对象,但是为什么说在重写equals()方法的同时也要重写hashCode()方法呢?实际上这只是一条规范,如果不这样做程序也可以执行,只不过会隐藏bug。一般一个类的对象如果会存储在HashTable,HashSet,HashMap等散列存储结构中,那么重写equals后最好也重写hashCode,否则会导致存储数据的不唯一性(存储了两个equals相等的数据)。而如果确定不会存储在这些散列结构中,则可以不重写hashCode。但是个人觉得还是重写比较好一点,谁能保证后期不会存储在这些结构中呢,况且重写了hashCode也不会降低性能,因为在线性结构(如ArrayList)中是不会调用hashCode,所以重写了也不要紧,也为后期的修改打了补丁。下面我们会通过具体的例子来进行分析与说明。

例子1 equals()方法和hashCode()方法均没有重写

public class EqualTest {

public static void main(String[] args){
HashSet<Student> set = new HashSet<Student>();
Student stu1 = new Student("0101101816","徐明");
Student stu2 = new Student("0101101816","徐明");
System.out.println("stu1==stu2: " + (stu1==stu2));
System.out.println("stu1.equals(stu2): " + (stu1.equals(stu2)));
System.out.println("stu1的哈希值: " + stu1.hashCode());
System.out.println("stu2的哈希值: " + stu2.hashCode());
set.add(stu1);
set.add(stu2);
System.out.println("set size: " + set.size());
}
}


public class Student {
private String id; //学号
private String name; //姓名
public Student(String id, String name) {
super();
this.id = id;
this.name = name;
}
}


运行结果如下:

stu1==stu2: false

stu1.equals(stu2): false

stu1的哈希值: 705927765

stu2的哈希值: 366712642

set size: 2

  分析:默认的hashCode()方法是根据对象的内存地址返回哈希值,因此两个不同对象的hashCode是不同的。默认的equals()是比较两个对象的内存地址,两个不同对象的地址当然不同,因此equals()方法的运行结果为false。由于hashCode()和equals()的运行结果均为不等,HashSet会认为这是两个不同的对象存入,因此set的长度为2。

例子2 现在重写hashCode()方法,以学生的学号作为返回hashCode的标准

public class Student {

private String id; //学号
private String name; //姓名

public Student(String id, String name) {
super();
this.id = id;
this.name = name;
}
/*
* 采用学号的哈希值作为返回值
*/
@Override
public int hashCode() {
// TODO Auto-generated method stub
return id.hashCode();
}
}


运行结果如下:

stu1==stu2: false

stu1.equals(stu2): false

stu1的哈希值: 528561005

stu2的哈希值: 528561005

set size: 2

  分析:由于两个学生对象的学号是一样的,因此得到的hashCode也是相同的。但是由于没有重写equals()方法,因此仍以两个对象的内存地址作为比较,因此equals()方法的运行结果仍为false。由于只有hashCode一致,equals()方法仍为false,因此Set会认为这是两个不同的对象,因此HashSet的长度仍为2。

例子3 现在只重写equals()方法,不重写hashCode()方法

public class Student {

private String id; //学号
private String name; //姓名

public Student(String id, String name) {
super();
this.id = id;
this.name = name;
}

@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(this==obj)
return true;
if(obj==null)
return false;
if(getClass() != obj.getClass())
return false;
final Student stu = (Student) obj;
if(this.id!=stu.id || this.name!=stu.name)
return false;
return true;
}
}


运行结果:

stu1==stu2: false

stu1.equals(stu2): true

stu1的哈希值: 705927765

stu2的哈希值: 366712642

set size: 2

  分析:结果重写equals()方法以后,很明显equals()方法的运行结果为true。而没有重写hashCode()方法时,hashCode()的返回值是以对象的内存地址作为返回值的,因此两个对象的哈希值不同。而此时set的长度依然为2,说明HashSet认为这是两个不同对象,为什么呢?这是因为HashSet、HashMap、HashTable这类的散列存储结构,我们可以认为里面是一个个的“桶”,根据java的机制会先调用hashCode()方法来确定元素所在的“桶”,然后再调用equals()方法来确定该桶内是否已经存在该元素。因此,虽然equals()方法为true,而hashCode方法返回值不一致时,Set会把元素存储到不同的“桶”内,所以Set的长度仍为2。这样就可以理解上述的第2条原则:若两个对象equals()返回true,则必须重写hashCode()也必须返回相同的int数。

例子4 equals()方法和hashCode()方法均重写

public class Student {

private String id; //学号
private String name; //姓名

public Student(String id, String name) {
super();
this.id = id;
this.name = name;
}

@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(this==obj)
return true;
if(obj==null)
return false;
if(getClass() != obj.getClass())
return false;
final Student stu = (Student) obj;
if(this.id!=stu.id || this.name!=stu.name)
return false;
return true;
}

/*
* 采用学号的哈希值作为返回值
*/
@Override
public int hashCode() {
// TODO Auto-generated method stub
return id.hashCode();
}
}


运行结果如下:

stu1==stu2: false

stu1.equals(stu2): true

stu1的哈希值: 528561005

stu2的哈希值: 528561005

set size: 1

  因此只有同时重写equals()方法和hashCode()方法时,才能保证Set集合认为这是同一个对象,Set长度为1。

例子4 下面是导致内存泄漏的代码

public class EqualTest {

public static void main(String[] args){
HashSet<Student> set = new HashSet<Student>();
Student stu1 = new Student("0101101816","徐明");
Student stu2 = new Student("0101101817","李亮");
System.out.println("stu1==stu2: " + (stu1==stu2));
System.out.println("stu1.equals(stu2): " + (stu1.equals(stu2)));
System.out.println("stu1的哈希值: " + stu1.hashCode());
System.out.println("stu2的哈希值: " + stu2.hashCode());
set.add(stu1);
set.add(stu2);
stu2.setId("0101101818");    //导致内存泄漏的代码
System.out.println("删除元素前set size: " + set.size());
set.remove(stu2);
System.out.println("删除元素后set size: " + set.size());
}
}


public class Student {
private String id; //学号
private String name; //姓名

public Student(String id, String name) {
super();
this.id = id;
this.name = name;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(this==obj)
return true;
if(obj==null)
return false;
if(getClass() != obj.getClass())
return false;
final Student stu = (Student) obj;
if(this.id!=stu.id || this.name!=stu.name)
return false;
return true;
}

/*
* 采用学号的哈希值作为返回值
*/
@Override
public int hashCode() {
// TODO Auto-generated method stub
return id.hashCode();
}
}


运行结果如下:

stu1==stu2: false

stu1.equals(stu2): false

stu1的哈希值: 528561005

stu2的哈希值: 528561006

删除元素前set size: 2

删除元素后set size: 2

  分析:从运行结果可以看出来,删除stu2元素前后的集合长度均为2,说明stu2没有删除成功。其实原因很容易理解,Set的remove操作同样是先调用hashCode()方法找到元素所在的“桶”,再调用equals()方法确定“桶”内是否存在该元素。假设stu2原先是在“桶2”,但是由于修改了学号id,导致其hashCode发生了变化,执行remove操作时会到“桶10”中寻找stu2,“桶10”中当然没有stu2,这样就导致删除不成功,因此Set的长度没有发生变化。

  因此我们可以得到一个结论:如果我们将对象的属性值参与了hashCode的运算中,在进行删除的时候,就不能对其属性值进行修改。如果非要修改,则必须先从集合中删除,更新信息后再加入集合中。否则会使用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: