[Java] Object方法浅析(一): equals与hashCode

摘要

  • equals描述的是一种等价关系,不仅仅是引用相等
  • equals重载需要满足自反性、对称性与传递性
  • 任何实例equals(null)需返回false,且多次调用equals返回值不变
  • hashCode返回对象的哈希值,多次调用hashCode返回值不变
  • hashCode不一定与内存地址相关
  • hashCode会发生碰撞
  • hashCode与equals密切相关
    • equals返回true的两个实例,hashCode值必须一样
    • hashCode值一样的两个实例,其equals方法返回不一定为true(可能有哈希碰撞等因素)

equals

equals是Object类的一个方法,常用来比较两个对象是否"相等"。当然,equals方法描述的相等是广义的,实际上应该是离散数学中的等价关系

等价关系(equivalence relation)

等价关系概念

等价关系等同于集合的划分,简单来说就是物品分类,比如定义一个车辆集合Cars,定义分类规则(二元关系)为同种颜色的车,那么所有红色的车划分为一堆(等价类),所有绿色的车为另一堆。每辆车都存在且只存在于一堆中,所有堆的合起来等同于Cars。

等价关系 - 同种颜色车辆

划分的好处在于每个等价类中的个体都是等价的,均能代表整个类(如红色的普桑可以代表红色的车)。且等价类之间互斥,任何一个元素均只包含在一个等价类中。

equals重载

等价关系不同于相等,它在不同的类中可以有不同的规则,equals在不同类中也被不同重载。

在Object类中,只有同一个对象才被认定为等价。

Object.class  
public boolean equals(Object obj) {
    return (this == obj);
}

而在Integer类中,同为Integer类且值相等则便认为等价,而不仅仅同一个引用。

Integer.class
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

但无论规则是什么,等价关系必须满足定义的三大原则:

1. 自反性:对于任何非空的x,x.equals(x)都应该返回true。
2. 对称性:对于任何非空x和y,当且仅当x.equals(y)返回true时,    y.equals(x)也应该返回true。
3. 传递性:对于任何非空x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。

此外,对于Java来说,还需满足另外两个原则:

4. 一致性:如果x和y的引用没有发生变化,多次调用x.equals(y)的结果应该相同。
5. 关于null:对于任何非空的x,x.equals(null)都应该返回false。

例如自定义Person类,当身份证identity号相等时候便可以认为两个实例等价,可以这样写:

 static class Person {

    public String name;
    public long identity;

    public Person(String name, long identity) {
        this.name = name;
        this.identity = identity;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            if (((Person) obj).identity == this.identity) {
                return true;
            }
        }
        return false;
    }
}

  测试类 
  @Test
  public void testEqual() throws Exception {
        Person x = new Person("aa", 1234);
        Person y = new Person("aa", 12345);
        Person z = new Person("bb", 1234);
        Assert.assertTrue(x.equals(x));
        Assert.assertTrue(x.equals(z));
        Assert.assertTrue(z.equals(x));
        Assert.assertFalse(x.equals(y));
        Assert.assertFalse(x.equals(null));
    }

hashCode

hashCode方法返回的是对象的哈希值。

Object类的hashcode方法取决于JVM的实现,比较典型的一种实现是基于内存地址进行哈希运算,此外也有基于伪随机数的实现。

需要注意的是hashCode与equals一样,多次调用不改变返回值。所以每个对象一旦计算出其identity hash code之后,在该对象死之前都必须保持同一个identity hash code值不可以改变,而不是每次基于内存地址运算(JVM GC会影响内存地址)。

hashCode 与 equals联系

hashCode与equals方法密切相关:两个equals为true的实例必须返回相同的hashCode。这在HashMap等类的使用中是非常有用的。

套用之前的Person类,我们知道Set具有去重功能,但是如果不重写hashCode,纵然两个Person实例是等价的,也是不能达到去重的效果

public void testHash() throws Exception {
    HashSet<Person> set = new HashSet<>();
    Person a = new Person("123", 123);
    Person b = new Person("123", 123);
    set.add(a);
    set.add(b);
    System.out.print("set size : " + set.size());
}

 输出 
 set size : 2

下面重写hashCode方法使其与equals方法相关

@Override
public int hashCode() {
      return (int) identity;
}

重新run后输出
set size : 1

显然这样比较符合人的习惯,这种影响广泛存在于基于hashCode的容器类中,例如HashMap的contains方法等。

所以,一般来说重写了equals方法就需要重写hashCode方法。比如Object中equals判断的是否为相同的引用,因此hashCode基于引用内存地址返回。在Integer中判断的是Integer的value值,因此hashCode值则直接返回的是value值。

哈希碰撞

但需要注意的是,hashCode相同的两个实例,equals方法不一定会返回true。因为hashCode是一种空间映射函数,空间大的数据映射至小空间则势必会产生哈希碰撞。

空间映射

举个简单的例子

String.class
public int hashCode() {
    int h = hash;
    if (h == 0 && count > 0) {
        for (int i = 0; i < count; i++) {
            h = 31 * h + charAt(i);
        }
        hash = h;
    }
    return h;
}

上述代码为String源码,String的hashCode方法上转换成函数等同于:

hashCode = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]  

简单来说就是讲其字符转换成31进制。

我们这样构造两个String实例

public void testHash() throws Exception {
    char A = 1;
    char B = 2;
    char C = 33;

    String aString = String.valueOf(A) + String.valueOf(B);
    String bString = String.valueOf(C);

    System.out.println("aString length: " + aString.length());
    System.out.println("bString length: " + bString.length());
    System.out.println("aString equals bString: " + aString.equals(bString));
    System.out.println("aString hashCode: " + aString.hashCode());
    System.out.println("bString hashCode: " + bString.hashCode());
}

输出为:

aString length: 2
bString length: 1
aString equals bString: false
aString hashCode: 33
bString hashCode: 33

显然,两个完全不一样的String对象产生可哈希碰撞。

关于数字31的引申

String equals方法中选31也是为了减少哈希碰撞,引用Effective Java中的原话来说

《Effective Java》

之所以选择31,是因为它是个奇素数。
如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。
使用素数的好处并不是很明显,但是习惯上都使用素数来计算散列结果。
31有个很好的特性,就是用移位和减法来代替乘法,可以得到更好的性能:31*i==(i<<5)-i。现在的VM可以自动完成这种优化。

关于奇数,在计算机中,一个数乘偶数表现为该数字左移n位,余位补0,因此可能造成信息丢失。比方说,一个二进制数字X4就可能导致两位数字丢失。

1010 0110  ->  10011000

而奇数则没有这个问题,因为任何一个奇数都可以转换为2的n次方+1,表现在计算机中则是左移n位再加上自己。

所以我们同样可以选择31进制作为hashCode的一种计算方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容