hashCode 和 equals 方法

Java所有类的父类Object类拥有如下重要的方法:

方法 描述
protected Object clone() 克隆对象
protected void finalize() 垃圾回收在销毁对象前调用
boolean equals(Object obj) 用于判断两个对象相等
int hashCode() 返回对象的hash值
String toString() 返回一个字符串表示对象
void notify() 唤醒等待在对象上的线程
void notifyAll() 唤醒所有等待在对象上的线程
void wait() 让线程阻塞知道notify被调用

equals 方法 和 ==

在比较对象时,操作符 == 比较的是两个对象的引用地址,只有当两个引用指向同一个对象时才相等。而 equals 方法允许用户自定义对象的比较方法。比如

final String str1 = new String( "bbb" );
System.out.println( "Using == operator: " + ( str1 == "bbb" ) );
System.out.println( "Using equals() method: " + str1.equals( "bbb" ) );

的运行结果为

Using == operator: false
Using equals() method: true

从需求的角度来看,比较两个字符串当然是当二者中所有字符一一对应即为相等,所以应该用 equals 方法来判断其相等性。我们可以看看String类的equals方法的实现方式:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

这段代码简单明了:首先比较了二者的引用是不是一个,其次判断是不是String对象,如果是再次逐一比较各个字符。这种equals的实现方式是一种常见的工程实用方式。

定义一个equals方法按照最佳实践应该满足如下标准:

  1. 自反性,对象x必须和自己相等,即x.equals(x)返回true
  2. 对称性,如果对象x等于y,那么y也等于x,即x.equals(y)y.equals(x)都返回true
  3. 传递性,如果对象x等于yy等于z,那么x也等于z,即x.equals(y)y.equals(z)都返回true,那么x.equals(z)返回true
  4. 一致性,多次调用equals方法返回的结果必须一致,除非用于比较的属性发生了变化
  5. 不等于nullequals(null)返回false

很容易验证Stringequals满足以上五点,所以是一个好的定义。

Object类的equals方法跟==等价,言下之意即,如果一个类没有定义equals方法,那么用equals方法判断相等性就等价于用==,即比较二者的引用地址。Object.equals定义如下:

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

equals 实现

equals方法的实现需要根据对象表示的数据结构来确定,一般地,会将对象的所有成员变量作为计算依据,也会排除一些无关紧要的成员变量。比如在上文提到的String类中,equals方法就是计算每个字符的相等性。假设有一个类叫Person,它有三个成员叫做firstName, lastName, email,那么equals方法最好就是比较这三个成员变量是否一一相等,比如

public class Person {
    private final String firstName;
    private final String lastName;
    private final String email;

    public Person( final String firstName, final String lastName, final String email ) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    // Step 0: Please add the @Override annotation, it will ensure that your
    // intention is to change the default implementation.
    @Override
    public boolean equals( Object obj ) {
        // Step 1: Check if the 'obj' is null
        if ( obj == null ) {
            return false;
        }

        // Step 2: Check if the 'obj' is pointing to the this instance
        if ( this == obj ) {
            return true;
        }

        // Step 3: Check classes equality. Note of caution here: please do not use the
        // 'instanceof' operator unless class is declared as final. It may cause
        // an issues within class hierarchies.
        if ( getClass() != obj.getClass() ) {
            return false;
        }

        // Step 4: Check individual fields equality
        final Person other = (Person) obj;
        if ( email == null ) {
            if ( other.email != null ) {
                return false;
            }
        } else if( !email.equals( other.email ) ) {
            return false;
        }

        if ( firstName == null ) {
            if ( other.firstName != null ) {
                return false;
            }
        } else if ( !firstName.equals( other.firstName ) ) {
            return false;
        }

        if ( lastName == null ) {
            if ( other.lastName != null ) {
                return false;
            }
        } else if ( !lastName.equals( other.lastName ) ) {
            return false;
        }

        return true;
    }
}

hashCode 方法

hashCode方法的作用是生成一个hash值,在用于键值(Key-Value)对储存的时候获得更快的随机存取速度。最常见的应用就是数据结构HashMapHashSet,hash使得元素的随机访问时间复杂度是O(1),不用hash而去访问一个元素则是O(n),因为需要遍历一遍集合。

在Java中,如果一个对象覆盖(override)了equals方法,那么最好也覆盖hashCode方法,并且如果两个对象equals返回true,那么他们的hashCode返回也应该一样,因为在hash相关的数据结构中二者会被同时用到,接下来会详细描述。

哈希表是根据键(Key)而直接访问在内存存储位置的数据结构。

举例说明哈希表的原理,假设有一些字符串需要组织成一个集合:"Shanghai", "Beijing", "Chongqing", "Guangzhou", "Hangzhou", "Chengdu", "Zhengzhou"。最简单的方式是直接放在一个数组,

String[] array = new String[] {"Shanghai", "Beijing", "Chongqing", "Guangzhou", "Hangzhou", "Chengdu", "Zhengzhou"};

这时假如需要读取某一个元素,必须遍历整个数组才能确定某个元素是否包含在数组中,即时间复杂度 O(n),当需要存储的元素变得很多,查询变得很频繁时,这样的时间复杂度将是一个很严重的性能瓶颈。有什么办法加快这种随机访问呢,那就是哈希表。

在哈希表中,每个元素通过一个哈希方法得到一个哈希值,哈希值一般是一个int类型。哈希表定义了一个初始容量,假定为n,哈希表的初始存储是一个长度为n的数组,将每个元素的哈希值对n取余,就能得到一个取值范围为[0, n)的余数(如果取余运算得到一个负数,可以将该负数加上n后再次取余得到对应的正数)。哈希表将该余数作为元素的存储位置标号,把元素储存到对应标号的数组位置。比如,对于以上讨论的字符串,取n=8,求出其对应的标号:

String[] array = new String[] {"Shanghai", "Beijing", "Chongqing", "Guangzhou", "Hangzhou", "Chengdu", "Zhengzhou"};
int mod = 8;
for (String s : array) {
    System.out.println((s.hashCode() % mod + mod) % mod);
}

输出结果为

3
6
4
0
6
2
4

以此为标号存储这些元素的效果看起来就是

位置 存储
0 "Guangzhou"
1
2 "Chengdu"
3 "Shanghai"
4 "Chongqing" "Zhengzhou"
5
6 "Beijing" "Hangzhou"
7

从存储的结果可以看出两个事实:

  1. 有的位置可能没有存储任何元素(比如位置1,5,7)
  2. 有的位置存储了不止一个元素(比如位置4,6,这种情况叫做位置冲突)

哈希表这样的存储结构将加快元素访问速度,最好的情况是所有元素分配在不同位置上,当访问一个元素时,只要通过其哈希值取余就能得到其位置然后直接访问,复杂度为 O(1);最差的情况是所有元素被哈希到了同一个位置,通过哈希取余得到位置后哈希表还要逐一比较该位置上所有元素,这跟直接用数组存储没什么区别了,复杂度为 O(n) 。

以上哈希表的例子可以看出,即便是7个元素存储在容量为8的哈希表中也发生了两处位置冲突。位置冲突越多,访问元素的速度就越慢,哈希表效率就越差。为了获得较高的命中率,哈希表应该避免存储过多的元素,这在哈希表的原理中叫做负载因子。初始化一个哈希表需要两个参数,一是容量,二是负载因子。负载因子是一个0到1的小数,表示当元素的数量达到容量的多少比例就算超负载了。负载因子一般取0.75,因为超过这个值后,哈希表性能将大幅下降,详见HashTable Wiki。当哈希表的元素量比例达到负载因子时,哈希表将进行一次扩容,即加大容量然后重新将所有元素计算哈希值并归位。

从哈希表的原理可知要存储一个对象,需要两个方法,一是哈希方法,而是位置冲突了的比较方法。在Java中hashCode方法便是用于生成一个对象的哈希值的,而equals方法则是比较对象的。到此可以知道为什么在覆盖了equals方法最好也覆盖hashCode方法了,这是因为hashCode方法使得用哈希表存储对象的时候大幅提升访问速度。在Java中运用了哈希表的数据结构主要有HashMapHashSet

从哈希表的原理还可以得出,两个不同对象的hashCode结果可能会相同,但是二者equals返回一定是false

hashCode 实现

equals方法类似,hashCode也会将重要的成员变量拿来计算哈希值,首先可以看看String类的hashCode如何实现的

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;
}

String的字符存储在value成员变量中,成员变量hash是一个哈希值的缓存(如果计算过hash的值了,那么直接使用,否则计算哈希值存储到hash;可以缓存哈希值是因为String是一个final的类,其内部的字符是不会改变的,所以其哈希值也不会改变,只需要计算一次)。计算哈希值的方式是将哈希值乘以31,然后逐一加入字符的值。这是一种非常常见而且实用高效的方式。原因是

  1. 31是一个素数,逐一相乘加入各个成员变量,最终得到的哈希值出现重合的可能性低;
  2. 与31相乘可以被编译器优化,因为31 * h等价于(h << 5) - h,而后者的运算速度比前者的乘法要快很多。

Java提供了一些便捷方式来实现equalshashCode,比如可以将Person类的这两个方法写成

@Override
public boolean equals( Object obj ) {
    if ( obj == null ) {
        return false;
    }

    if ( this == obj ) {
        return true;
    }

    if ( getClass() != obj.getClass() ) {
        return false;
    }

    final PersonObjects other = (PersonObjects) obj;
    if( !Objects.equals( email, other.email ) ) {
        return false;
    } else if( !Objects.equals( firstName, other.firstName ) ) {
        return false;            
    } else if( !Objects.equals( lastName, other.lastName ) ) {
        return false;            
    }

    return true;
}

@Override
public int hashCode() {
    return Objects.hash( email, firstName, lastName );
}

可以查看Objects的源码发现其hash方法的实现:

// Objects.java
public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

// Arrays.java
public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

可以看到Objects.hash的实现方式正是上文讨论的逐一乘以31的方式。

应用

equals方法主要在比较两个对象时用到,hashCode主要在哈希表相关数据结构中用到,详见HashSetHashMap
equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。换句话说,equals()方法不相等的两个对象,hashCode()有可能相等。(我的理解是由于哈希码在生成的时候产生冲突造成的)

反过来:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object类中,hashcode()方法是本地方法,返回的是对象的地址值,而object类中的equals()方法比较的也是两个对象的地址值,如果equals()相等,说明两个对象地址值也相等,当然hashcode() 也就相等了

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

推荐阅读更多精彩内容