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

来源:互联网 发布:我思考所以我存在知乎 编辑:程序博客网 时间:2024/05/22 00:31

  我们知道在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()方法总结如下:

  1. 若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
  2. 若两个对象equals()返回true,则hashCode()也必须返回相同的int数。
  3. 若两个对象equals()返回false,则hashCode()不一定返回不同的int数。
  4. 若两个对象hashCode()返回相同int数,则equals()不一定返回true。
  5. 若两个对象hashCode()返回不同int数,则equals()一定返回false。
  6. 同一对象在执行期间若已经存储在集合中,则不能修改影响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的运算中,在进行删除的时候,就不能对其属性值进行修改。如果非要修改,则必须先从集合中删除,更新信息后再加入集合中。否则会使用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。

原创粉丝点击