hashcode重写

707 阅读4分钟

这篇文章是以前写的,从segmentfault搬运过来的,因为我的seg上只有一篇文章,孤零零的,所以搬过来。

我的第一篇文章

以前都在公司的文档里写,后来想想,还是自己找个地方记录下来吧。 今天有个朋友问我hashcode的问题,记录下来,并稍微读下书寻求一点理论知识。

问题如下

有一个属性都是字符串的对象,想放入hashset中,要求,对某一个属性,相同就能放入,不同就不能放入。

朋友的问题是,知道equals咋写,但是不知道hashcode咋写,没有思路。

我的理解(仅供借鉴)

hashcode是一种比equals粗粒度的比较,打个比方,两个三位数,可以拿十位+个位数作为hashcode,百位+十位+个位才是真正的equals。也就是说,先比较十位+个位数hashcode,如果hashcode不一致,那么这两个对象必然不一致,就不用继续对比了,如果十位+个位数一致,那么他们有可能是一致的,这时继续对比equals,才能知道是否两个对象真的一致。 当然这只是一种粗浅的理解,真正的理解还得看**((h = key.hashCode()) ^ (h >>> 16)) & (n-1)**,直观上可以理解为就是hash值的补码取后几位,问题也不大。

public class InnerClass {
    String a;
    String b;
    String c;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }

        InnerClass that = (InnerClass)o;
        return this.a.equals(that.a);
    }

如果只是寻求一个解,而不是寻求较优的解的话,那么很简单。不是要比equals粗粒度的比较吗?随便来一个比equals粗一丁点的就可以了。如下

    @Override
    public int hashCode() {
        return Objects.hash(a);
    }

这个写法得到的结果是对的,但是我总觉得不好,因为在我的理解里,hashcode也是一种效率上的考虑。这样的话,和直接比较this.a又有多大区别呢?

如果强行要粗粒度,我想了一种方法,例如:

    @Override
    public int hashCode() {
        return Objects.hash(a)/3;
    }

这样其实也增加不了效率,因为真正要比较的可以理解为hashcode的后几位,除不除以3,影响不大。

假设问题变成:对a和b相同的不放入set,那么hashcode用Object.hash(a)才是一种相对不错的方式。既满足了要求,又有一定的效率上的提升。

这么闭门造车不是个办法,拜读一下《effective java》2e.

整理如下:
1.对同一个对象调用hashcode时,必须返回同样的值。
2.equals相同的对象,必须有相同的hashcode。
3.equals不同的对象,建议有不同的hashcode。
4.当不重写hashcode时,map.put(new A("a"), "b")之后,map.get(new A("a"))不一定能取到"b",因为没重写hashcode,put和get时的两个对象,都是用的Object的hashcode,因为两次new是两个不同的对象,所以hashcode不相同,落在不同的bucket。(即便恰好落在相同的bucket,也不一定能获取到值)
5.hashcode不要返回一个固定值。返回固定值会导致,所有值都落在同一个bucket,这样程序的时间复杂度会增加。所以最好是让hashcode均匀的落在不同的bucket。
6.hashcode均匀落在bucket的一个良好实践是:
----1.一个初始值
----2.对对象的每一个字段,计算一个值(boolean:f?1:0/byte、char、short、int (int)f/long int(f^(f>>>32))/double Double.doubleToLongBits(f),然后按long处理/对象,对对象的每个字段调用hashcode
----3.result = 31*result + c;
7.对不包含在equals里对比的字段,在hashcode中排除掉。
8.对有意义的字段(即equals里对比的字段),不要在hashcode中排除。例如要对a、b做equals,只对a做hashcode,那么可能hashcode在bucket的分布就不均匀了,性能就会下降。

根据这些规则,发现以前的很多想法都是错误的。正确写法如下:

@Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + a.hashCode();
        return result;
    }

要对a、b两个字段做联合去重时:

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + a.hashCode();
        result = 31 * result + b.hashCode();
        return result;
    }

皮一下:
a做equals,a、b做hashcode,放入hashset结果可能为
"b" "a" "e",
"a" "b" "c",
"b" "c" "e"
这种情况就是违反了第2条,放入了不同的bucket。

之前那个优化的想法不对(ab做equals,a做hash),因为b可能会导致hashcode在bucket的分布不均匀。

补充:bucket是什么? bucket就是hashmap中包含一系列entry的东西,每个bucket是一个链表或者一颗树。

TODO:
a.hashcode()
object.hash(a)
object.hashcode(a)
的区别