对象存储在hash集合中为什么要重写equals和hashcode方法

最近关注集合类,其中有很多底层数据结构是hash散列表,例如:hashMap, hashSet, hashTable,同时我也关注到这些类型存储的Key值是不能重复的(这里的重复是指hash值是否相等)
因为我们知道一个对象的hash值相等,equals()不一定是true

例子

我们就拿HashSet作为列子(只使用hashMap的key,有兴趣的可以自己搜一下)

基本数据类型
        Integer i1 = 1;
        Integer i2 = 1;
        System.out.println("equals: " + i1.equals(i2));
        System.out.println("hash值: " + (i1.hashCode() == i2.hashCode()));
        System.out.println("地址:" + (i1 == i2));
        HashSet hashSetI = new HashSet();
        hashSetI.add(i1);
        hashSetI.add(i2);
        System.out.println("HashSet中的内容: " + JSONObject.toJSONString(hashSetI));

结果:

equals: true
hash值: true
地址:true
HashSet中的内容: [1]

Integer重写hashcode()方法

    /**
     * Returns a hash code for this {@code Integer}.
     *
     * @return  a hash code value for this object, equal to the
     *          primitive {@code int} value represented by this
     *          {@code Integer} object.
     */
    @Override
    public int hashCode() {
        return Integer.hashCode(value);
    }

我们可以看到定义两个变量都赋值1,hash值相等,所以存在hashSet中的内容只有一个,同时因为我们看到地址是一样的所以引出一个Integer的常量池,看了源码中池子的容量是-128~127,当然可以通过修改JVM的参数来更改,这里就不过多介绍,想了解可以看下源码

    /**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }
基本数据类型
        String a1 = "1";
        String a2 = "1";
        System.out.println("equals: " + a1.equals(a2));
        System.out.println("hash值: " + (a1.hashCode() == a2.hashCode()));
        System.out.println("地址:" + (a1 == a2));
        HashSet hashSet1 = new HashSet();
        hashSet1.add(a1);
        hashSet1.add(a2);
        System.out.println(JSONObject.toJSONString(hashSet1));

结果:

equals: true
hash值: true
地址:true
HashSet中的内容: ["1"]

String类中的hashCode()方法

    /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

我们可以看到定义两个变量都赋值1,hash值相等,所以存的内容就是一个

实体类对象

首先定义一个类,仅有一个属性key

    static class NoOverEqAndHash {
        String key;

        public NoOverEqAndHash(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }

        public void setKey(String key) {
            this.key = key;
        }

    }

把对象存入hashset

        // 自定义一个类,不重写equals和hashcode方法
        NoOverEqAndHash noOverEqAndHashA = new NoOverEqAndHash("1");
        NoOverEqAndHash noOverEqAndHashB = new NoOverEqAndHash("1");
        System.out.println("equals: " + noOverEqAndHashA.equals(noOverEqAndHashB));
        System.out.println("hash值: " + (noOverEqAndHashA.hashCode() == noOverEqAndHashB.hashCode()));
        System.out.println("地址:" + (noOverEqAndHashA == noOverEqAndHashB));
        HashSet noOverHashSet = new HashSet();
        noOverHashSet.add(noOverEqAndHashA);
        noOverHashSet.add(noOverEqAndHashB);
        System.out.println("HashSet中的内容: " + JSONObject.toJSONString(noOverHashSet));

结果:

equals: false
hash值: false
地址:false
HashSet中的内容: [{"key":"1"},{"key":"1"}]
public class Object {
    public boolean equals(Object obj) {
        return (this == obj);
    }
}    

我们可以看到定义两个对象,属性都是一样的,但是equals方法返回结果是不相同,因为实体类继承Object类的equals()的方法没有重写,判断的是对象的地址是否相同

重写实体类对象的equals()和hashCode()

重写的方法:

    static class OverEqAndHash {

        String key;

        public OverEqAndHash(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }

        public void setKey(String key) {
            this.key = key;
        }

        @Override
        public boolean equals(Object obj) {
            if(this == obj){
                return true;//地址相等
            }

            if(obj == null){
                return false;//非空性:对于任意非空引用x,x.equals(null)应该返回false。
            }

            if(obj instanceof OverEqAndHash){
                OverEqAndHash other = (OverEqAndHash) obj;
                //需要比较的字段相等,则这两个对象相等
                if(equalsStr(this.key, other.key)){
                    return true;
                }
            }
            return false;
        }

        private boolean equalsStr(String str1, String str2){
            if(StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)){
                return true;
            }
            if(!StringUtils.isEmpty(str1) && str1.equals(str2)){
                return true;
            }
            return false;
        }

        @Override
        public int hashCode() {
            int result = 17;
            result = 31 * result + (key == null ? 0 : key.hashCode());
            return result;
        }
    }
        // 自定义一个类,重写equals和hashcode方法
        OverEqAndHash overEqAndHashA = new OverEqAndHash("1");
        OverEqAndHash overEqAndHashB = new OverEqAndHash("1");
        System.out.println("equals: " + overEqAndHashA.equals(overEqAndHashB));
        System.out.println("hash值: " + (overEqAndHashA.hashCode() == overEqAndHashB.hashCode()));
        System.out.println("地址:" + (overEqAndHashA == overEqAndHashB));
        HashSet overHashSet = new HashSet();
        overHashSet.add(overEqAndHashA);
        overHashSet.add(overEqAndHashB);
        System.out.println("HashSet中的内容: " + JSONObject.toJSONString(overHashSet));

结果:

equals: true
hash值: true
地址:false
HashSet中的内容: [{"key":"1"}]

我们可以看到定义两个对象,属性都是一样的,因为实体类继承Object类的equals()的方法,重写了equals方法和hashCode()方法所以存入的值就是一个

hashCode()源码(注释中已经说明了公式)

    /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

顺带解释一下hashCode()这样重写的原因:

  1. 其实当 equals 方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
  2. 生成一个 int 类型的变量 result,并且初始化一个值,比如17,对类中每一个重要字段,也就是影响对象的值的字段,也就是 equals 方法里有比较的字段,进行以下操作:
    a. 计算这个字段的值 filedHashValue = filed.hashCode();
    b. 执行 result = 31 * result + filedHashValue;

使用31为乘积的原因:
1:更少的乘积结果冲突
  31是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。 所以从 31,33,37,39 等中间选择了 31 的原因看原因二。
2:31 可以被 JVM 优化
  JVM里最有效的计算方式就是进行位运算了:
左移 << : 左边的最高位丢弃,右边补全0(把 << 左边的数据2的移动次幂)。
  右移 >> : 把>>左边的数据/2的移动次幂。
  无符号右移 >>> : 无论最高位是0还是1,左边补齐0。   
所以 : 31 * i = (i << 5) - i(左边 31
2=62,右边 2*2^5-2=62) - 两边相等,JVM就可以高效的进行计算啦==

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值