覆盖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的类,都应该有相应的单元测试去检查是否有没有违反上述约定。下面是个简单的例子:
- public class PhoneNumber {
- private int countryCode;
- private String nationalNumber;
- public PhoneNumber(){
- super();
- }
- public PhoneNumber(int countryCode, String nationalNumber) {
- super();
- this.countryCode = countryCode;
- this.nationalNumber = nationalNumber;
- }
- /**
- * @return the countryCode
- */
- public int getCountryCode() {
- return countryCode;
- }
- /**
- * @param countryCode the countryCode to set
- */
- public void setCountryCode(int countryCode) {
- this.countryCode = countryCode;
- }
- /**
- * @return the nationalNumber
- */
- public String getNationalNumber() {
- return nationalNumber;
- }
- /**
- * @param nationalNumber the nationalNumber to set
- */
- public void setNationalNumber(String nationalNumber) {
- this.nationalNumber = nationalNumber;
- }
- @Override
- public boolean equals(Object o){
- if(this == o){
- return true;
- }
- if(!(o instanceof PhoneNumber)){
- return false;
- }
- PhoneNumber pn = (PhoneNumber) o;
- return this.countryCode == pn.getCountryCode()
- && ( this.nationalNumber == null ? pn.nationalNumber == null : this.nationalNumber.equals(pn.nationalNumber));
- }
- }
- import static org.junit.Assert.*;
- import org.junit.Test;
- public class PhoneNumberTest {
- @Test
- public void testEqualsReflexive(){
- PhoneNumber pn1 = new PhoneNumber(86, "12345");
- assertTrue(pn1.equals(pn1));
- PhoneNumber pn2 = new PhoneNumber();
- assertTrue(pn2.equals(pn2));
- }
- @Test
- public void testEqualsSymmetric(){
- PhoneNumber pn1 = new PhoneNumber(86, "12345");
- PhoneNumber pn2 = new PhoneNumber(86, "12345");
- assertEquals(pn1.equals(pn2), pn2.equals(pn1));
- }
- @Test
- public void testEqualsTransitive(){
- PhoneNumber pn1 = new PhoneNumber(86, "12345");
- PhoneNumber pn2 = new PhoneNumber(86, "12345");
- PhoneNumber pn3 = new PhoneNumber(86, new String("12345"));
- assertTrue(pn1.equals(pn2));
- assertTrue(pn2.equals(pn3));
- assertTrue(pn1.equals(pn3));
- }
- @Test
- public void testEqualsConsistent(){
- PhoneNumber pn1 = new PhoneNumber(86, "12345");
- PhoneNumber pn2 = new PhoneNumber(86, "12345");
- for(int i=0; i<10 ; i++){
- assertTrue(pn1.equals(pn2));
- }
- }
- @Test
- public void testEqualsWithNull(){
- PhoneNumber pn1 = new PhoneNumber(86, "12345");
- assertFalse(pn1.equals(null));
- }
- }
当然还有一些实现高质量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类为例,编写了如下的测试用例:
- @Test
- public void testHashCode(){
- PhoneNumber pn1 = new PhoneNumber(86, "12345");
- PhoneNumber pn2 = new PhoneNumber(86, "12345");
- Map<PhoneNumber,String> map = new HashMap<PhoneNumber,String>();
- map.put(pn1, "12345");
- assertNotNull(map.get(pn2));
- }
发现测试失败了,但是两个对象通过equals比较是相等的。由于并没有覆盖hashCode方法导致两个相等的对象不能获得相同的散列码。根据约定重写hashCode:
- @Override
- public int hashCode(){
- int result = 17;
- result = 31 * result + countryCode;
- if(nationalNumber != null)
- result = 31 * result + nationalNumber.hashCode();
- return result;
- }
好的散列函数通常倾向于"为不相等的对象产生不相等的散列码", 否则会引起冲突,使散列表想链表退化。计算是可以把冗余的域排除在外。注意不要试图从散列码计算中排除关键域来提高性能。