`
boyitech
  • 浏览: 84002 次
  • 性别: Icon_minigender_1
  • 来自: 南通
社区版块
存档分类
最新评论

博弈Java讲义 - 关于equals & hash

    博客分类:
  • Java
阅读更多

覆盖equals方法和hashCode方法看似简单,但其实不然,如果没有按照jdk的通用规范去覆盖,那么基于这些约定的类将可能无法正常工作,例如基于散列的集合类HashMap和HashSet. 
  对于值类,我们通常需要覆盖Object.equals方法,因为我们希望通过equals方法知道它们在逻辑上是否相等.相应的这个类的实例可以被用作map的key,或者set的元素的时候才会表现出预期的行为. 对于"值类",枚举是个例外,因为枚举的每个值都是个单例. 
  在覆盖equals时,必须遵守JavaSE Object的规范:自反性(reflective), 对称性 (symmetric),传递性(transitive),一致性(consistent),对于任何非null的引用x, x.equals(null)返回false. 对于每个覆盖类equals的类,都应该有相应的单元测试去检查是否有没有违反上述约定。下面是个简单的例子: 

Java代码 
  1. public class PhoneNumber {  
  2.   
  3.     private int countryCode;  
  4.     private String nationalNumber;  
  5.       
  6.     public PhoneNumber(){  
  7.         super();  
  8.     }  
  9.       
  10.     public PhoneNumber(int countryCode, String nationalNumber) {  
  11.         super();  
  12.         this.countryCode = countryCode;  
  13.         this.nationalNumber = nationalNumber;  
  14.     }  
  15.     /** 
  16.      * @return the countryCode 
  17.      */  
  18.     public int getCountryCode() {  
  19.         return countryCode;  
  20.     }  
  21.     /** 
  22.      * @param countryCode the countryCode to set 
  23.      */  
  24.     public void setCountryCode(int countryCode) {  
  25.         this.countryCode = countryCode;  
  26.     }  
  27.       
  28.     /** 
  29.      * @return the nationalNumber 
  30.      */  
  31.     public String getNationalNumber() {  
  32.         return nationalNumber;  
  33.     }  
  34.     /** 
  35.      * @param nationalNumber the nationalNumber to set 
  36.      */  
  37.     public void setNationalNumber(String nationalNumber) {  
  38.         this.nationalNumber = nationalNumber;  
  39.     }  
  40.     @Override  
  41.     public boolean equals(Object o){  
  42.         if(this == o){  
  43.             return true;  
  44.         }  
  45.         if(!(o instanceof PhoneNumber)){  
  46.             return false;  
  47.         }  
  48.         PhoneNumber pn = (PhoneNumber) o;  
  49.         return this.countryCode == pn.getCountryCode()  
  50.                 && ( this.nationalNumber == null ?  pn.nationalNumber == null : this.nationalNumber.equals(pn.nationalNumber));  
  51.     }     
  52. }  
  53.   
  54. import static org.junit.Assert.*;  
  55.   
  56. import org.junit.Test;  
  57.   
  58. public class PhoneNumberTest {  
  59.   
  60.     @Test  
  61.     public void testEqualsReflexive(){  
  62.         PhoneNumber pn1 = new PhoneNumber(86"12345");  
  63.         assertTrue(pn1.equals(pn1));  
  64.         PhoneNumber pn2 = new PhoneNumber();  
  65.         assertTrue(pn2.equals(pn2));  
  66.     }  
  67.       
  68.     @Test  
  69.     public void testEqualsSymmetric(){  
  70.         PhoneNumber pn1 = new PhoneNumber(86"12345");  
  71.         PhoneNumber pn2 = new PhoneNumber(86"12345");  
  72.         assertEquals(pn1.equals(pn2), pn2.equals(pn1));  
  73.     }  
  74.       
  75.       
  76.     @Test  
  77.     public void testEqualsTransitive(){  
  78.         PhoneNumber pn1 = new PhoneNumber(86"12345");  
  79.         PhoneNumber pn2 = new PhoneNumber(86"12345");  
  80.         PhoneNumber pn3 = new PhoneNumber(86new String("12345"));  
  81.         assertTrue(pn1.equals(pn2));  
  82.         assertTrue(pn2.equals(pn3));  
  83.         assertTrue(pn1.equals(pn3));  
  84.     }  
  85.       
  86.     @Test  
  87.     public void testEqualsConsistent(){  
  88.         PhoneNumber pn1 = new PhoneNumber(86"12345");  
  89.         PhoneNumber pn2 = new PhoneNumber(86"12345");  
  90.         for(int i=0; i<10 ; i++){  
  91.             assertTrue(pn1.equals(pn2));  
  92.         }  
  93.     }  
  94.       
  95.     @Test  
  96.     public void testEqualsWithNull(){  
  97.         PhoneNumber pn1 = new PhoneNumber(86"12345");  
  98.         assertFalse(pn1.equals(null));  
  99.     }  
  100.       
  101. }  



当然还有一些实现高质量equals方法的诀窍: 
   1. 使用==操作符检查"参数是否为正确的引用" 
   2. 使用instanceof检查类型 
   3. 把参数转化为正确的类型 
   4. 选择逻辑比较的关键域,注意比较的顺序,primitive的比较可以放在前面,或者最有可能不一致性的域 
   5. 如果有double,float类型,用Double.compare,Float.compare比较 
   6. 覆盖equals重要覆盖hashCode

 

如前文所述,在覆盖了equals方法的类中,也必须覆盖hashCode方法。否则违反了Object.hashCode的通用约定会导致该类无法和基于散列的集合(HashMap,HashSet和HashTable)一起正常使用。 
  如下约定内容摘自Object规范: 
  1. 在应用程序中,只要对象的euqals方法的比较操作所用的信息没有修改,那么对于同一个对象的调用多次hashCode,必须始终如一返回同一个哈希值。 
  2. 如果两个对象通过equals比较相等,那么它们的哈希值相同。 
  3. 如果两个对象通过euqals比较不等,他们的哈希值可能相同,取决于hashCode的实现,由此散列表的性能也会有区别。 
  以前面的PhoneNumber类为例,编写了如下的测试用例: 
  
 

Java代码 
  1. @Test  
  2.   public void testHashCode(){  
  3.     PhoneNumber pn1 = new PhoneNumber(86"12345");  
  4.     PhoneNumber pn2 = new PhoneNumber(86"12345");  
  5.     Map<PhoneNumber,String> map = new HashMap<PhoneNumber,String>();  
  6.     map.put(pn1, "12345");  
  7.     assertNotNull(map.get(pn2));  
  8.   }  



  发现测试失败了,但是两个对象通过equals比较是相等的。由于并没有覆盖hashCode方法导致两个相等的对象不能获得相同的散列码。根据约定重写hashCode: 

Java代码 
  1. @Override  
  2. public int hashCode(){  
  3.     int result = 17;  
  4.     result = 31 * result + countryCode;  
  5.     if(nationalNumber != null)  
  6.     result = 31 * result + nationalNumber.hashCode();  
  7.     return result;  
  8. }  


  
  好的散列函数通常倾向于"为不相等的对象产生不相等的散列码", 否则会引起冲突,使散列表想链表退化。计算是可以把冗余的域排除在外。注意不要试图从散列码计算中排除关键域来提高性能。 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics