龙珠

修炼自己与发现世界

孪生兄弟:How to override equals and hashCode Method in Java?

本来只想写一下如何重写equals函数,不过查阅的资料里也谈到了equals和hashCode要保持一致性,索性两个一起写了。

在我们写自己的类的时候,一般都需要重写equals和hashCode方法。因为在Object类中,这二者都是对地址信息进行判断的,所以,即便内容相同的两个对象也是判断不同的。

查看Object.java中的equals代码,重写equals方法有如下要求:

1) Reflexive : Object must be equal to itself.

2) Symmetric : if a.equals(b) is true then b.equals(a) must be true.

3) Transitive : if a.equals(b) is true and b.equals(c) is true then c.equals(a) must be true.

4) Consistent : multiple invocation of equals() method must result same value until any of properties are modified. So if two objects are equals in Java they will remain equals until any of there property is modified.

5) Null comparison : comparing any object to null must be false and should not result in NullPointerException.

根据这一原则,之前查Hash资料的时候,有看到重写equals方法的几步法。代码如下:

@Override

public boolean equals(Object obj) {

// TODO Auto-generated method stub

if(this == obj){

return true;

}

if(obj instanceof A){

A other = (A)obj;

return this.myString.equals(other.myString) &&

this.myInt == other.myInt;

}

return false;

}

可以看到,重写分为几步:

1. 使用“==”检查是否是相同引用;

2. 使用instanceof关键字,检查是否是相同类。这里有个tip,就是instanceof关键字会自动包含null检查,即:如果obj是null,则自动返回false。

3. 若为相同类,则对类内的值域进行判断。举例代码中使用的是myString和myInt。

我一开始认为这样的步骤很完美,判断没什么错误。但是后来查别人写的equals代码,发现很多人对obj进行null判断和getClass判断,不解。查了资料才明白。

在How to override equals and hashCode method in Java - Example, Tips中提到:

after some research I found that instead of instanceof we can use getClass() method for type identification because instanceof check returns true for subclass also, so its not strictly equals comparison until required by business logic. But instanceof check is fine if your class is immutable and no one is going to sub class it.

也就是说,instanceof关键字对该类的子类也会判断为true,这样不同的类也会判断 ,容易出问题。而且要求条件比较 ,需要保证该类“immutable”并且没有子类才行。

所以更好的办法是,替代代码,使用null和getClass方法检查。我修改后新代码如下:

@Override

public boolean equals(Object obj) {

// TODO Auto-generated method stub

if(this == obj){

return true;

}

if((obj != null || this.getClass() == obj.getClass())){

A other = (A)obj;

return this.myString.equals(other.myString) &&

this.myInt == other.myInt ;

}

return false;

}

但在实验中报错!!

测试后才发现,原文的代码如下:

if((obj == null) || (obj.getClass() != this.getClass()))

       return false;

而我的代码如下:

if((obj != null || this.getClass() == obj.getClass())){

A other = (A)obj;

return this.myString.equals(other.myString) &&

this.myInt == other.myInt ;

}

return false;

这里就有个问题,就是如果obj不为null的话,就不会进行下一步判断getClass。所以要么把||改为&&,要么使用原文代码。

最终,重写equals的代码如下:

@Override

public boolean equals(Object obj) {

// TODO Auto-generated method stub

if(this == obj){

return true;

}

if((obj == null || this.getClass() != obj.getClass())){

return false;

}

A other = (A)obj;

return this.myString.equals(other.myString) &&

this.myInt == other.myInt;

}

好了,equals修改完毕。不过,工作还没有完。在Object.java文件中,对equals方法的文档有如下:

    * Note that it is generally necessary to override the {@code hashCode}

    * method whenever this method is overridden, so as to maintain the

    * general contract for the {@code hashCode} method, which states

    * that equal objects must have equal hash codes.

也就是说,只要equals方法被override,则根据“general contract for the hashCode method”,均需要再重写hashCode方法。

我的理解是,这是因为,在HashMap这样的哈希表中,需要保证equals和hashCode的结果相同,这样才能保证相同的key在哈希表中的位置是相同的。否则,二者结果不同,在哈希表中存储会发生错误和冲突。如:equals相同的两个的对象的hashCode不同,于是两个相同key的对象的存储位置不同,无法更新和获取数据。

那“general contract for the hashCode method”又是什么呢?

在Object.java文件中,hashCode方法的文档就有对此的描述:

    * The general contract of {@code hashCode} is:

    * <ul>

    * <li>Whenever it is invoked on the same object more than once during

    *     an execution of a Java application, the {@code hashCode} method

    *     must consistently return the same integer, provided no information

    *     used in {@code equals} comparisons on the object is modified.

    *     This integer need not remain consistent from one execution of an

    *     application to another execution of the same application.

    * <li>If two objects are equal according to the {@code equals(Object)}

    *     method, then calling the {@code hashCode} method on each of

    *     the two objects must produce the same integer result.

    * <li>It is <em>not</em> required that if two objects are unequal

    *     according to the {@link java.lang.Object#equals(java.lang.Object)}

    *     method, then calling the {@code hashCode} method on each of the

    *     two objects must produce distinct integer results.  However, the

    *     programmer should be aware that producing distinct integer results

    *     for unequal objects may improve the performance of hash tables.

即主要包括以下几点:

1. 在一次Java application运行过程中,多次调用hashCode方法应返回相同int值。【疑问:对”provided no information used in {@code equals} comparisons on the object is modifiedd“应该如何理解?对象修改后,equals和hashCode改变吗?是否同时改变?我认为应该是同时改变的】

2. 若两个对象equals返回true,则他们的hashCode结果必须相同;

3. 若两个对象equals返回false,并不一定要求他们的hashCode结果不同。当然,hashCode不同会在使用hash table等的哈希表时,提高效率。

而对于如何重写hashCode函数,还是等下一系列的Hash系列文章来说吧。

另外,在How to override equals and hashCode method in Java中,还提到重写equals方法时常犯的错误,包括:

1. 重载(overload)函数而非重写(override)函数。应该override的Object中的equals方法为: public boolean equals(Object obj){}。有可能出错的是参数类型,写成 public boolean equals(Person person){},造成overload而非override。

2. 在check值域之前没有先check null。这样会导致nullPointerException。

3. 没有同时重写equals和hashCode方法。这样在使用HashMap等哈希表的时候会出错。

4. 没有保持equals方法和compareTo方法的一致性(consistent)。这是个非正式要求,主要用来obey contract of Set to avoid duplicates。

SortedSet implementation like TreeSet uses compareTo to compare two objects like String and if compareTo() and equals() will not be consistent than TreeSet will allow duplicates which will break Set contract of not having duplicates.

最后,Override equals and hashCode methods文章中,还提到eclipse里还可以通过” Source | Generate hashCode() and equals()... “来自动生成hashCode和equals方法的代码。我试了一下,可以根据选择的值域来重写这两个方法,还是挺好玩的。

参考资料:

  1. How to override equals and hashCode method in Java

  2. Override equals and hashCode methods