Java 常识(012):hashCode 和 equals 的区别

时间:2019-02-17
本文章向大家介绍Java 常识(012):hashCode 和 equals 的区别,主要包括Java 常识(012):hashCode 和 equals 的区别使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Java的基类Object提供了一些方法,其中equals()方法用于判断两个对象是否相等,hashCode()方法用于计算对象的哈希码。equals()和hashCode()都不是final方法,都可以被重写(overwrite)。

1、equals 方法

Object类中equals()方法实现如下:

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

1.1、equals 的通用约定

虽然我们可以重写 equals()方法,但是根据Object 的规范要求,必须遵守它的通用约定:

(1)自反性(reflexive):对于任何非null的引用值x,x.equals(x)必须返回true。

(2)对称性(symmetric):对于任何非null的引用值x和y,x.equals(y)与y.equals(x)的返回值必须相等。

(3)传递性(transitive):对于任何非null的引用值x、y、z,如果 x.equals(y)为true,并且y.equals(z)也为true,那么x.equals(z)必须为true。

(4)一致性(consistent):对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true,或者一致的返回false

(5)对于任何非null的引用x,x.equals(null)必须返回false。

1.2、equals 和 == 的区别

== : 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是一个对象。

equals() : 它的作用也是判断两个对象是否相等,但它一般有两种使用情况

1)类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。

2)类覆盖了equals()方法。我们都覆盖equals()方法来比较两个对象的内容相等,若它们的内容相等,则返回true。

public static void main(String[] args) {
        String str1 = "Hello World";
        String str2 = new String("Hello World");
        String str3 = new String("Hello World");
        String str4 = "Hello World";
        System.out.println(str1 == str2); // false
        System.out.println(str2 == str3); // false
        System.out.println(str2.equals(str3)); // true
        System.out.println(str1 == str4); // true
        str4 = "Happy new year";
        String str5 = "Happy new year";
        System.out.println(str1 == str4); // false
        System.out.println(str4 == str5); // true
    }

1.3、重写高质量equals方法的诀窍

(1)使用==操作符检查“参数是否为这个对象的引用”

如果是对象本身,则直接返回,拦截了对本身调用的情况,算是一种性能优化。

(2)使用instanceof操作符检查“参数是否是正确的类型”

如果不是,就返回false,正如对称性和传递性举例子中说得,不要想着兼容别的类型,很容易出错。在实践中检查的类型多半是equals所在类的类型,或者是该类实现的接口的类型,比如Set、List、Map这些集合接口。

(3)把参数转化为正确的类型

因为转换之前进行过instanceof 判断,所以确保会成功

(4)对于该类中的“关键域”,检查参数中的域是否与对象中的对应域相等

基本类型的域就用==比较,float域用Float.compare方法,double域用Double.compare方法,至于别的引用域,我们一般递归调用它们的equals方法比较,加上判空检查和对自身引用的检查,一般会写成这样:(field == o.field || (field != null && field.equals(o.field))),而上面的String里使用的是数组,所以只要把数组中的每一位拿出来比较就可以了。

(5)编写完成后思考是否满足的对称性,传递性,一致性。

注意事项:

1)覆盖equals时一定要覆盖hashCode;

2)equals函数里面一定要是Object类型作为参数;

3)equals方法本身不要过于智能,只要判断一些值相等即可

2、hashCode 方法

Object类中hashCode()方法的声明如下:

public native int hashCode();

hashCode()是一个native方法,而且返回值类型是整形;实际上,该native方法将对象在内存中的地址转换为哈希码返回,可以保证不同对象的返回值不同。

2.1、覆盖equals时总要覆盖hashCode

一个很常见的错误就在于没有覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致无法结合所有基于散列的集合一起正常运作,这样的集合包括 HashMap、HashSet 和 HashTable。

Object.hashCode的通用约定如下:

(1)在java应用执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一对象调用多次hashCode方法都必须始终如一地同一个整数。在同一个应用程序的多次执行过程中,每次执行该方法返回的整数可以不一致。

(2)如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。

(3)如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法没必要产生不同的整数结果。但是程序猿应该知道,给不同的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。

2.1.1、示例演示不覆盖hashCode的后果

public class PhoneNumber {
    private int areaCode;
    private int prefix;
    private int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof PhoneNumber)) {
            return false;
        }
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber
                && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    public static void main(String[] args) {
        // 1)初始化
        Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
        // 2)put存储
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        // 3)get获取
        System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
    }
}

此时,我们期望的输出结果是 Jenny ,实际上输出结果却是 null。

这里使用了两个 PhoneNumber 实例对象,一个被用于插入到 HashMap,另一个实例被用来从 HashMap 获取值。由于 PhoneNumber 类没有重写 hashCode 方法,导致了两个实例具有不同的散列值,违反了 hashCode 的约定。

2.1.2、示例演示覆盖hashCode

public class PhoneNumber {
    private int areaCode;
    private int prefix;
    private int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof PhoneNumber)) {
            return false;
        }
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber
                && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }

    public static void main(String[] args) {
        // 1)初始化
        Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
        // 2)put存储
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        // 3)get获取
        System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
    }
}

这里为什么选用 31 作为基础系数呢,这里借鉴了String中对hashCode的重写,具体原因见此文:https://segmentfault.com/a/1190000010799123?utm_source=tag-newest

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

2.3、hashCode 的作用

hashCode用于返回对象的hash值,主要用于查找的快捷性,因为hashCode是Object对象中的方法,所以所有Java对象都有hashCode,在HashTable和HashMap这一类的散列结构中,都是通过hashCode来查找在散列表中的位置的。

当我们向哈希表(如HashSet、HashMap等)中添加对象object时,首先调用hashCode()方法计算object的哈希码,通过哈希码可以直接定位object在哈希表中的位置(一般是哈希码对哈希表大小取余)。如果该位置没有对象,可以直接将object插入该位置;如果该位置有对象(可能有多个,通过链表实现),则调用equals()方法比较这些对象与object是否相等,如果相等,则不需要保存object;如果不相等,则将该对象加入到链表中。

这也就解释了为什么equals()相等,则hashCode()必须相等。如果两个对象equals()相等,则它们在哈希表(如HashSet、HashMap等)中只应该出现一次;如果hashCode()不相等,那么它们会被散列到哈希表的不同位置,哈希表中出现了不止一次。

在JVM中,加载的对象在内存中包括三部分:对象头、实例数据、填充。

对象头包括指向对象所属类型的指针和MarkWord,而MarkWord中除了包含对象的GC分代年龄信息、加锁状态信息外,还包括了对象的hashcode;

对象实例数据是对象真正存储的有效信息;

填充部分仅起到占位符的作用,原因是HotSpot要求对象起始地址必须是8字节的整数倍。

如果两个对象equals,那么它们的hashCode必然相等,
但是hashCode相等,equals不一定相等。

2.4、重写高质量hashCode方法的诀窍

一个好的hashCode的方法更倾向于:为不相等的对象产生不相等的散列码,同样为相等的对象产生相等的散列码。

(1)把某个非零的常数值,比如17,保存在一个int型的result中;

(2)对于每个关键域f(equals方法中涉及到的每个域),完成以下操作:

a、为该域计算int类型的散列码

 i.如果该域是boolean类型,则计算(f?1:0),
 ii.如果该域是byte,char,short或者int类型,计算(int)f,
 iii.如果是long类型,计算(int)(f^(f>>>32)).
 iv.如果是float类型,计算Float.floatToIntBits(f).
 v.如果是double类型,计算Double.doubleToLongBits(f),然后再计算long型的hash值
 vi.如果是对象引用,则递归的调用域的hashCode,如果是更复杂的比较,则需要为这个域计算一个范式,然后针对范式调用hashCode,如果为null,返回0
 vii. 如果是一个数组,则把每一个元素当成一个单独的域来处理。

b、按照下面的公式,把 a 中计算得到的散列码合并到result中

result=31 * result+c

(3)返回result

(4)编写单元测试验证有没有实现所有相等的实例都有相等的散列码。

3、hashCode 与 equals 的关系

以下是Object对象API关于equal方法和hashCode方法的说明:

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.

Java对象的eqauls方法和hashCode方法是这样规定的:

1、相等(equals)的对象必须具有相同的哈希码(hashCode)。

假如两个Java对象A和B,A和B相等(eqauls结果为true),但A和B的哈希码不同,则A和B存入HashMap时的哈希码计算得到的HashMap内部数组位置索引就会不同,那么A和B很有可能允许同时存入HashMap,显然相同的元素是不允许同时存入HashMap,HashMap不允许存放重复元素。

2、如果两个对象的哈希码(hashCode)相同,它们并不一定相等(equals)。

假如两个Java对象A和B不相等(eqauls结果为false),但A和B的哈希码相等,将A和B都存入HashMap时会发生哈希冲突,也就是A和B存放在HashMap内部数组的位置索引相同这时HashMap会在该位置建立一个链接表,将A和B串起来放在该位置,显然,该情况不违反HashMap的使用原则,是允许的。当然,哈希冲突越少越好,尽量采用好的哈希算法以避免哈希冲突。

在object类中,hashcode()方法是本地方法,返回的是对象的地址值,而object类中的equals()方法比较的也是两个对象的地址值,如果equals()相等,说明两个对象地址值也相等,当然hashcode()也就相等了;

在String类中,equals()返回的是两个对象内容的比较,当两个对象内容相等时,Hashcode()方法根据String类的重写代码的分析,hashcode()返回结果也会相等。以此类推,可以知道Integer、Double等封装类中经过重写的equals()和hashcode()方法也同样适合于这个原则。当然没有经过重写的类,在继承了object类的equals()和hashcode()方法后,也会遵守这个原则。