重写equal()时为什么也得重写hashCode()之深度解读equal方法与hashCode方法渊源(二)

文章来源:

https://blog.csdn.net/javazejian/article/details/51348320

继续:

https://blog.csdn.net/u013412772/article/details/80321068




4.equals()的重写规则之必要性深入解读

前面我们一再强调了equals方法重写必须遵守的规则,接下来我们就是分析一个反面的例子,看看不遵守这些规则到底会造成什么样的后果。

package com.zejian.test;  
import java.util.ArrayList;  
import java.util.List;  
/** * 反面例子 * @author zejian */  
public class AbnormalResult {  
    public static void main(String[] args) {  
        List<A> list = new ArrayList<A>();  
        A a = new A();  
        B b = new B();  
        list.add(a);  
        System.out.println("list.contains(a)->" + list.contains(a));  
        System.out.println("list.contains(b)->" + list.contains(b));  
        list.clear();  
        list.add(b);  
        System.out.println("list.contains(a)->" + list.contains(a));  
        System.out.println("list.contains(b)->" + list.contains(b));  
    }  
    static class A {  
        @Override  
        public boolean equals(Object obj) {  
            return obj instanceof A;  
        }  
    }  
    static class B extends A {  
        @Override  
        public boolean equals(Object obj) {  
            return obj instanceof B;  
        }  
    }  
}  

上面的代码,我们声明了 A,B两个类,注意必须是static,否则无法被main调用。B类继承A,两个类都重写了equals方法,但是根据我们前面的分析,这样重写是没有遵守对称性原则的,我们先来看看运行结果:


list.contains(a)->true

list.contains(b)->false

list.contains(a)->true

list.contains(b)->true

19行和24行的输出没什么好说的,将a,b分别加入list中,list中自然会含有a,b。但是为什么20行和23行结果会不一样呢?我们先来看看contains方法内部实现

@Override         
public boolean contains(Object o) {   
     return indexOf(o) != -1;   
}  

进入indexof方法

      @Override  
public int indexOf(Object o) {  
E[] a = this.a;  
if (o == null) {  
    for (int i = 0; i < a.length; i++)  
        if (a[i] == null)  
            return i;  
} else {  
    for (int i = 0; i < a.length; i++)  
        if (o.equals(a[i]))  
            return i;  
}  
return -1;  

可以看出最终调用的是对象的equals方法,所以当调用20行代码list.contains(b)时,实际上调用了

b.equals(a[i]),a[i]是集合中的元素集合中的类型而且为A类型(只添加了a对象),虽然B继承了A,但此时

a[i] instanceof B 

结果为false,equals方法也就会返回false;而当调用23行代码list.contains(a)时,实际上调用了a.equal(a[i]),其中a[i]是集合中的元素而且为B类型(只添加了b对象),由于B类型肯定是A类型(B继承了A),所以

a[i] instanceof A  

结果为true,equals方法也就会返回true,这就是整个过程。但很明显结果是有问题的,因为我们的 list的泛型是A,而B又继承了A,此时无论加入了a还是b,都属于同种类型,所以无论是contains(a),还是contains(b)都应该返回true才算正常。而最终却出现上面的结果,这就是因为重写equals方法时没遵守对称性原则导致的结果,如果没遵守传递性也同样会造成上述的结果。当然这里的解决方法也比较简单,我们只要将B类的equals方法修改一下就可以了。

static class B extends A{  
    @Override  
    public boolean equals(Object obj) {  
        if(obj instanceof B){  
            return true;  
        }  
        return super.equals(obj);  
    }  
}  

到此,我们也应该明白了重写equals必须遵守几点原则的重要性了。当然这里不止是list,只要是java集合类或者java类库中的其他方法,重写equals不遵守5点原则的话,都可能出现意想不到的结果。

5.为什么重写equals()的同时还得重写hashCode()

这个问题之前我也很好奇,不过最后还是在书上得到了比较明朗的解释,当然这个问题主要是针对映射相关的操作(Map接口)。学过数据结构的同学都知道Map接口的类会使用到键对象的哈希码,当我们调用put方法或者get方法对Map容器进行操作时,都是根据键对象的哈希码来计算存储位置的,因此如果我们对哈希码的获取没有相关保证,就可能会得不到预期的结果。在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。那我们先来认识一下hashCode()这个方法吧。hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的,如果x与y是两个不同的对象,那么x.hashCode()与y.hashCode()基本是不会相同的,下面通过String类的hashCode()计算一组散列码:

package com.zejian.test;  
public class HashCodeTest {  
    public static void main(String[] args) {  
        int hash=0;  
        String s="ok";  
        StringBuilder sb =new StringBuilder(s);  

        System.out.println(s.hashCode()+"  "+sb.hashCode());  

        String t = new String("ok");  
        StringBuilder tb =new StringBuilder(s);  
        System.out.println(t.hashCode()+"  "+tb.hashCode());  
    }  

} 

运行结果:

3548  1829164700

3548  2018699554

我们可以看出,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不同的散列码,这是因为StringBuilder没有重写hashCode方法,它的散列码是由Object类默认的hashCode方法计算出来的对象存储地址,所以散列码自然也就不同了。那么我们该如何重写出一个较好的hashCode方法呢,其实并不难,我们只要合理地组织对象的散列码,就能够让不同的对象产生比较均匀的散列码。例如下面的例子:

package com.zejian.test;  
public class Model {  
    private String name;  
    private double salary;  
    private int sex;  

    @Override  
    public int hashCode() {  
        return name.hashCode()+new Double(salary).hashCode()   
                + new Integer(sex).hashCode();  
    }  
}  

上面的代码我们通过合理的利用各个属性对象的散列码进行组合,最终便能产生一个相对比较好的或者说更加均匀的散列码,当然上面仅仅是个参考例子而已,我们也可以通过其他方式去实现,只要能使散列码更加均匀(所谓的均匀就是每个对象产生的散列码最好都不冲突)就行了。不过这里有点要注意的就是java 7中对hashCode方法做了两个改进,首先java发布者希望我们使用更加安全的调用方式来返回散列码,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,这个方法的优点是如果参数为null,就只返回0,否则返回对象参数调用的hashCode的结果。Objects.hashCode 源码如下:

public static int hashCode(Object o) {  
    return o != null ? o.hashCode() : 0;  
}  

因此我们修改后的代码如下:

package com.zejian.test;  
import java.util.Objects;  
public  class Model {  
    private   String name;  
    private double salary;  
    private int sex;  
    @Override  
    public int hashCode() {  
        return Objects.hashCode(name)+new Double(salary).hashCode()   
                + new Integer(sex).hashCode();  
    }  
}  

java 7还提供了另外一个方法java.util.Objects.hash(Object… objects),当我们需要组合多个散列值时可以调用该方法。进一步简化上述的代码:

package com.zejian.test;  
import java.util.Objects;  
public  class Model {  
    private   String name;  
    private double salary;  
    private int sex;  
//  @Override  
//  public int hashCode() {  
//      return Objects.hashCode(name)+new Double(salary).hashCode()   
//              + new Integer(sex).hashCode();  
//  }  

    @Override  
    public int hashCode() {  
        return Objects.hash(name,salary,sex);  
    }  
}  

好了,到此hashCode()该介绍的我们都说了,还有一点要说的如果我们提供的是一个数值类型的变量的话,那么我们可以调用Arrays.hashCode()来计算它的散列码,这个散列码是由数组元素的散列码组成的。接下来我们回归到我们之前的问题,重写equals方法时也必须重写hashCode方法。在Java API文档中关于hashCode方法有以下几点规定(原文来自java深入解析一书)。

  • 在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
  • 如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
  • 如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。

通过前面的分析,我们知道在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等,对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。因此到这里我们就明白了,如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果。如果大家不相信, 可以看看下面的例子(来自java深入解析一书)

package com.zejian.test;  
import java.util.HashMap;  
import java.util.Map;  
public class MapTest {  
    public static void main(String[] args) {  
        Map<String,Value> map1 = new HashMap<String,Value>();  
        String s1 = new String("key");  
        String s2 = new String("key");    
        Value value = new Value(2);  
        map1.put(s1, value);  
        System.out.println("s1.equals(s2):"+s1.equals(s2));  
        System.out.println("map1.get(s1):"+map1.get(s1));  
        System.out.println("map1.get(s2):"+map1.get(s2));  


        Map<Key,Value> map2 = new HashMap<Key,Value>();  
        Key k1 = new Key("A");  
        Key k2 = new Key("A");  
        map2.put(k1, value);  
        System.out.println("k1.equals(k2):"+s1.equals(s2));  
        System.out.println("map2.get(k1):"+map2.get(k1));  
        System.out.println("map2.get(k2):"+map2.get(k2));  
    }  

    /** 
     * 键 
     * @author zejian 
     * 
     */  
    static class Key{  
        private String k;  
        public Key(String key){  
            this.k=key;  
        }  

        @Override  
        public boolean equals(Object obj) {  
            if(obj instanceof Key){  
                Key key=(Key)obj;  
                return k.equals(key.k);  
            }  
            return false;  
        }  
    }  

    /** 
     * 值 
     * @author zejian 
     * 
     */  
    static class Value{  
        private int v;  

        public Value(int v){  
            this.v=v;  
        }  

        @Override  
        public String toString() {  
            return "类Value的值-->"+v;  
        }  
    }  
}  

代码比较简单,我们就不过多解释了(注意Key类并没有重写hashCode方法),直接运行看结果

s1.equals(s2):true  
map1.get(s1):类Value的值-->2  
map1.get(s2):类Value的值-->2  
k1.equals(k2):true  
map2.get(k1):类Value的值-->2  
map2.get(k2):null  

对于s1和s2的结果,我们并不惊讶,因为相同的内容的s1和s2获取相同内的value这个很正常,因为String类重写了equals方法和hashCode方法,使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了,k1获取到的值是2,k2获取到的是null,这是为什么呢?想必大家已经发现了,Key只重写了equals方法并没有重写hashCode方法,这样的话,equals比较的确实是内容,而hashCode方法呢?没重写,那就肯定调用超类Object的hashCode方法,这样返回的不就是地址了吗?k1与k2属于两个不同的对象,返回的地址肯定不一样,所以现在我们知道调用map2.get(k2)为什么返回null了吧?那么该如何修改呢?很简单,我们要做也重写一下hashCode方法即可(如果参与equals方法比较的成员变量是引用类型的,则可以递归调用hashCode方法来实现):

@Override  
public int hashCode() {  
     return k.hashCode();  
} 

再次运行:

s1.equals(s2):true  
map1.get(s1):类Value的值-->2  
map1.get(s2):类Value的值-->2  
k1.equals(k2):true  
map2.get(k1):类Value的值-->2  
map2.get(k2):类Value的值-->2  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值