对java中equals和hashCode函数的一些理解

时间:2022-02-15 20:03:08
JDK的java.lang.Object类中实现了equals函数,其定义说明如下: 
引用
    public boolean equals(Object obj) 
         Indicates whether some other object is "equal to" this one. 
         The equals method implements an equivalence relation on non-null object references: 
          [list=5] 
         
  • It  is reflexive: for any non-null reference value x, x.equals(x) should return true.                      
  • It  is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.         
  • It  is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.         
  • It  is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.         
  • For  any non-null reference value x, x.equals(null) should return false.          [/list] 
             The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true). 
             Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes. 


  • 其具体的实现代码如下所示: 
    Java代码  对java中equals和hashCode函数的一些理解
    1. public boolean equals(Object obj) {  
    2.     return (this == obj);  
    3. }    

         从上面的代码中可以看出,Object类的equals实现只是简单地调用了“==”运算符,也即定义中说明的,对于两个非空对象X和Y,有且仅当X和Y指向同一个对象时该函数才返回true。由于Object类是java中所有类的超类,因而其它任何类都默认继承了此函数。在大多数情况下,此默认的实现方式是合适的,因为任一个类本质上都是唯一的;但是对于那些表示“值”的类(如Integer、String等),它们引入了自己特有的“逻辑相等”的概念,此时就必须修改equals函数的实现。 
    例如java.lang.String类的实现如下: 
    Java代码  对java中equals和hashCode函数的一些理解
    1.    public boolean equals(Object anObject) {  
    2.        if (this == anObject) {  
    3.     return true;  
    4. }  
    5. if (anObject instanceof String) {  
    6.     String anotherString = (String)anObject;  
    7.     int n = count;  
    8.     if (n == anotherString.count) {  
    9.     char v1[] = value;  
    10.     char v2[] = anotherString.value;  
    11.     int i = offset;  
    12.     int j = anotherString.offset;  
    13.     while (n-- != 0) {  
    14.         if (v1[i++] != v2[j++])  
    15.         return false;  
    16.     }  
    17.     return true;  
    18.     }  
    19. }  
    20. return false;  
    21.    }  

    而java.lang.Integer类的实现如下: 
    Java代码  对java中equals和hashCode函数的一些理解
    1. public boolean equals(Object obj) {  
    2.     if (obj instanceof Integer) {  
    3.         return value == ((Integer)obj).intValue();  
    4.     }  
    5.     return false;  
    6.     }  

          String类的equals实现使得当两个String对象所表示的字符串的值是一样的时候就能返回true;而Integer类则当两个Integer对象所持有的整数值是一样的时候就能返回true。 
          因此,当我们我们自己编写的类需要引入“逻辑相等”的概念,且其超类又没有改写equals的实现时,我们就必须实现自己的equals函数。在上面Object类中关于equals函数的说明中,定义了5条改写equals函数所必须遵守的规范。其中第1条自反性(reflexivity)和第5条通常能够自行满足。下面我们重点介绍下第2点对称性(symmetric)、第3点传递性(transitive)和第4点一致性(consistent)。 
           对称性  
    即对于任意两个引用值X和Y,X.equals(Y) 和 Y.equals(X) 返回的结果必须是一致的。下面引用《effect java》上的例子来说明了违反这一特性的后果: 
    Java代码  对java中equals和hashCode函数的一些理解
    1. public class CaseInsensitiveString {  
    2.       
    3.     private String s;  
    4.       
    5.     public boolean equals(Object obj){  
    6.         if(obj instanceof CaseInsensitiveString)  
    7.             return s.equalsIgnoreCase(((CaseInsensitiveString)obj).s);  
    8.         if(obj instanceof String)  
    9.             return s.equalsIgnoreCase((String)obj);  
    10.         return false;  
    11.     }  
    12. }  

    然后我们定义如下的类来进行测试: 
    Java代码  对java中equals和hashCode函数的一些理解
    1. public void test(){  
    2.     CaseInsensitiveString cis = new CaseInsensitiveString("Value");  
    3.     String s = "Value";  
    4.     List list = new ArrayList();  
    5.     list.add(cis);  
    6.     System.out.println(cis.equals(s));  
    7.     System.out.println(s.equals(cis));  
    8.     System.out.println(list.contains(s));  
    9. }  

    运行test(),第一条语句的输出为true,而后两条为false。因为CaseInsensitiveString类中equals函数对String对象的比较也可能返回true,只要其大小写不敏感的字符串值相等;而在String类中对任何非String对象都返回false,这样CaseInsensitiveString的实现就违反了对称性,从而导致了第三条语句也返回了意想不到的结果false。这是因为ArrayList的contains函数实现是用实参s对list中每一个对象调用equals函数,若有equals函数返回true,则contains返回true;因而在这里contains函数必然返回false。因此,如果违反了对称性,则可能会得到意料之外的结果。解决此问题的一个办法就是在CaseInsensitiveString类的equals函数中对Stirng对象的比较无论值是否一致都返回false。 
           传递性  
    即对于任意的引用值X、Y和Z,如果 X.equals(Y) 和 Y.equals(Z) 都返回true,则 X.equals(Z) 也一定返回true。下面举例说明: 
    Java代码  对java中equals和hashCode函数的一些理解
    1.  public class Point {  
    2.     private final int x;  
    3.   
    4.     private final int y;  
    5.   
    6.     public Point(int x, int y) {  
    7.         this.x = x;  
    8.         this.y = y;  
    9.     }  
    10.   
    11.     public boolean equals(Object obj) {  
    12.         if (!(obj instanceof Point))  
    13.             return false;  
    14.         Point p = (Point) obj;  
    15.         return p.x == x && p.y == y;  
    16.     }  
    17. }  
    18.   
    19. public class ColorPoint extends Point {  
    20.   
    21.     private int z;  
    22.   
    23.     public ColorPoint(int x, int y, int z) {  
    24.         super(x, y);  
    25.         this.z = z;  
    26.     }  
    27.   
    28.     public boolean equals(Object obj) {  
    29.         if (!(obj instanceof ColorPoint))  
    30.             return false;  
    31.         ColorPoint cp = (ColorPoint) obj;  
    32.         return super.equals(obj) && cp.z == this.z;  
    33.     }  
    34. }  

    在上面的代码中定义了类Point及其子类ColorPoint,并分别改写了equals函数。下面运行如下代码来测试: 
    Java代码  对java中equals和hashCode函数的一些理解
    1. ColorPoint cp1 = new ColorPoint(1,2,3);  
    2. ColorPoint cp2 = new ColorPoint(1,2,4);  
    3. Point p = new Point(1,2);  
    4. System.out.println(cp1.equals(p));  
    5. System.out.println(p.equals(cp2));  
    6. System.out.println(cp1.equals(cp2));  

    结果前两条语句输出“true”,而最后一条语句输出为“false”;从而违反了传递性规范。其主要原因是在基类的equals函数中并未对子类加以区分,从而使得基类与子类之间的比较也能返回true。要解决此方法,《effect java》上提供了“复合代替继承”的方法;另外,如果可以将基类和子类视为不等的话,使用 getClass()函数代替 instanceof 运算符进行比较可以很好的解决这一问题。getClass()会返回此对象的运行期类(runtime class),因为基类和子类属于不同的class,getClass()能够将其区分开来。下面是一段示例的代码: 
    Java代码  对java中equals和hashCode函数的一些理解
    1. public boolean equals(Object obj) {  
    2.         if(obj.getClass() != this.getClass())  
    3.             return false;  
    4.         Point p = (Point) obj;  
    5.         return p.x == x && p.y == y;  
    6.           
    7.     }  

           一致性  
    即如果两个对象相等的话,那么它们必须始终保持相等,除非至少有一个对象被修改了。 

    在Object类equals函数的说明中的最后一段提到当我们改写equals函数的时候,通常都需要改写hashCode函数,后者同样在Object类中进行了定义,如下: 
    引用
    public int hashCode() 
    Returns a hash code value for the object. This method is supported for the benefit of hashtables such as those provided by java.util.Hashtable. 
    The general contract of hashCode is: 
    [list=3]
  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in 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.
  • If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the 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 hashtables.[/list] 
    As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.) 

  • hashCode()函数返回一个对象的散列值(hash code),在java中有些集合类都是基于散列值的,如HashMap、HashSet、Hashtable等;它们都根据对象的散列值将其映射到相应的散列桶中。如Hashtable的put和get函数的实现如下所示: 
    Java代码  对java中equals和hashCode函数的一些理解
    1.   public synchronized V get(Object key) {  
    2. Entry tab[] = table;  
    3. int hash = key.hashCode();  
    4. int index = (hash & 0x7FFFFFFF) % tab.length;  
    5. for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {  
    6.     if ((e.hash == hash) && e.key.equals(key)) {  
    7.     return e.value;  
    8.     }  
    9. }  
    10. return null;  
    11.    }  
    12.   
    13. ublic synchronized V put(K key, V value) {  
    14. // Make sure the value is not null  
    15. if (value == null) {  
    16.     throw new NullPointerException();  
    17. }  
    18.   
    19. // Makes sure the key is not already in the hashtable.  
    20. Entry tab[] = table;  
    21. int hash = key.hashCode();  
    22. int index = (hash & 0x7FFFFFFF) % tab.length;  
    23. for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {  
    24.     if ((e.hash == hash) && e.key.equals(key)) {  
    25.     V old = e.value;  
    26.     e.value = value;  
    27.     return old;  
    28.     }  
    29. }  
    30.   
    31. modCount++;  
    32. if (count >= threshold) {  
    33.     // Rehash the table if the threshold is exceeded  
    34.     rehash();  
    35.   
    36.            tab = table;  
    37.            index = (hash & 0x7FFFFFFF) % tab.length;  
    38. }   
    39.   
    40. // Creates the new entry.  
    41. Entry<K,V> e = tab[index];  
    42. tab[index] = new Entry<K,V>(hash, key, value, e);  
    43. count++;  
    44. return null;  
    45.    }  

    因而hashCode函数极大地影响了这些集合类的正常工作。如果在改写完equals函数后,不相应改写hashCode函数,则可能会得不到想要的结果。如下例所示: 
    Java代码  对java中equals和hashCode函数的一些理解
    1. Point p1 = new Point(12);  
    2. Point p2 = new Point(12);  
    3. Hashtable<Point, String> ht = new Hashtable<Point, String>();  
    4. ht.put(p1, "value");  
    5. System.out.println(p1.equals(p2));  
    6. System.out.println(ht.get(p2));  

    由上面Point类的实现,p1和p2是相等的,第一个语句正常输出true;但是第二个语句输出的值是null,并没有得到期望中的“value”。导致这一问题的根本原因是Point类改写了equals函数,对相等的概念作了更改,但没有相应更改hashCode函数,使得两个相等的函数拥有不同的hashCode,违反了Object类关于hashCode的约定中的第2点,从而返回了错误的结果。 
    在Object类中定义的几个hashCode约定如下: 
    1. 在同一应用中,一个对象的hashCode函数在equals函数没有更改的情况下,无论调用多少次,它都必须返回同一个整数。 
    2. 两个对象如果调用equals函数是相等的话,那么调用hashCode函数一定会返回相同的整数。 
    3. 两个对象如果调用equals函数是不相等的话,那么调用hashCode函数不要求一定返回不同的整数。 
    我们在改写equals 和 hashCode 函数的时候,一定要遵守如上3条约定,在改写equals的同时也改写hashCode的实现,这样才能保证得到正确的结果。