深入理解Java的 == equals() hashCode() - Go语言中文社区

深入理解Java的 == equals() hashCode()


源码注释

虽然网上关于这些知识点的博客不少,但我们还是先从JDK的源码注释下手,看看源码注释足以带给我们多少有用的信息。充分理解JDK作者的话以后,我们再去博众家之长。

hashCode()的注释如下:

Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.
The general contract of hashCode is:

  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
  • 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 java.lang.Object#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.

As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.)

翻译过来就是(尽量不直译,我会用自己的话说,想直译自己谷歌翻译):
hashCode()就是用来返回对象的哈希值的(注意是对象,所以基本类型就没法调hashCode了)。这个方法是为了支持那些可哈希的容器,比如java.util.HashMap(因为其他的那些不可哈希的容器,根本不会去调元素类型的hashCode方法)。

  • 在一个java程序中的同一次执行过程中,只要equals这个比较方法在比较时用到的信息没有被修改掉,那么hashCode在同一个对象上会永远返回同一个int值。但如果同一个java程序的不同执行过程里,同一个对象的hashCode是可能改变的(比如,IDEA里你点了run后又点了stop,那么下一次run就是不同的执行过程了)。
  • 如果两个对象通过equals(Object)比较后返回了true,那么这两个对象的hashCode必须返回同一个int值(当然,前提是:它们两个比较返回true以后,equals用到的信息没有被改变)。
  • 如果两个对象通过equals(Object)比较后返回了false,但这两个对象的hashCode不一定能返回不同的int值。但是,两个unequal的对象如果能返回不同的hashCode是最好的,因为这样能提高哈希容器的性能,减少哈希冲突。

equals()的注释如下:

Indicates whether some other object is “equal to” this one.
The equals method implements an equivalence relation on non-null object references:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).
Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

翻译过来就是:
equals()是用来判断其他对象是否和此对象相等。在非空的对象引用上,equals()有如下一些的对等关系:

  • 自反性。对于非空引用xx.equals(x)应该返回true。
  • 对称性。对于非空引用xy,如果x.equals(y)返回了true,那么y.equals(x)也会返回true。
  • 可传递性。对于非空引用xyz,如果x.equals(y)返回了true,且y.equals(z)返回了true,那么x.equals(z)也会返回true。
  • 一致性。对于非空引用xy,只要未修改对象的equals比较中使用的信息(两个对象都没有修改),那么返回结果永远为同一个值(true or false)。
  • 对于非空引用xx.equals(null)应该返回false。

Object类实现了最严格的equals方法,因为对于非空引用xy,只有当xy引用同一个对象时,equals方法才会返回true。(因为其方法实现为public boolean equals(Object obj) { return (this == obj); }
通常,在重写equals方法后,是有必要再重写hashCode方法的。因为hashCode的约束就是,两个equal的对象必须有相同的hashCode(之后解释为什么)。

从上面的信息可以画出下面这个图:
在这里插入图片描述
从图中我们可以轻易地得出下面的结论:

  • 如果两个对象是equal的,那么能推导出,两个对象的HashCode也相同。
  • 如果两个对象的HashCode相同,不能推导出,两个对象是equal的。
  • 如果两个对象是unequal的,不能推导出,两个对象的HashCode也不同。
  • 如果两个对象的HashCode不同,那么能推导出,两个对象是unequal的。

== 与 最严格的equals

前面说了Object类实现了最严格的equals方法,因为其方法实现为public boolean equals(Object obj) { return (this == obj); }

==在比较时主要分为两种情况:

  • 两边为基本数据类型,那么比较其值。
  • 两边为引用数据类型,那么比较引用指向对象的内存地址(即地址值)。只有当两个引用指向同一个对象时,这两个引用指向对象的内存地址才会相等。

所以才说,Object类实现了最严格的equals方法,因为其方法实现用到了==,而==要返回true必须左右两边的引用指向同一个对象。

== 与 泛型类

在用==比较两个泛型类对象时,还需要把泛型的类型参数考虑进去,比如new ArrayList<String>() == new ArrayList<Integer>()编译时会报错,因为ArrayList<String>类型和ArrayList<Integer>类型是不同的两种类型(就是尖括号里面不一样啦)。

我们去java spec里找答案,https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.21,发现章节Reference Equality Operators == and !=里面有这句话:

It is a compile-time error if it is impossible to convert the type of either operand to the type of the other by a casting conversion (§5.5). The run-time values of the two operands would necessarily be unequal (ignoring the case where both values are null).

翻译过来就是:在用==比较两个引用时,如果既不存在左边操作数可以隐式转换为右边操作数类型的情况,也不存在右边操作数可以隐式转换为左边操作数类型的情况,那么将会产生编译错误

所以上面的new ArrayList<String>() == new ArrayList<Integer>()会编译报错,就是因为不存在从ArrayList<String>类型到ArrayList<Integer>类型的隐式转换。

        //boolean b1= Integer.class == String.class;//编译报错
        boolean b2= (Class)Integer.class == String.class;
        boolean b3= (Class<?>)Integer.class == String.class;
        boolean b= (Object)Integer.class == String.class;
        //boolean b4= (Class<? extends Integer>)Integer.class == String.class;//编译报错
        //boolean b5= (Class<? super Integer>)Integer.class == String.class;//编译报错

举一反三地,我们结合上面==的知识点和泛型的知识点,来分析一下上面例子的情况:

  • Integer.class == String.class编译报错。因为左边类型是Class<Integer>,右边类型是Class<String>,这二者相互之间不可以隐式转换的,所以报错。
  • (Class)Integer.class == String.class返回false。左边类型被强转为泛型类的原生类型raw type,然后右边类型是可以隐式转换为原生类型的,所以编译通过。(Class aa = String.class能通过编译则证明了此观点)
  • (Class<?>)Integer.class == String.class返回false。分析同上,左边类型强转为泛型类的无边界的通配符,然后右边类型也是可以隐式转换为泛型类的无边界的通配符的,所以编译通过。(Class<?> aa = String.class能通过编译则证明了此观点)
  • (Object)Integer.class == String.class返回false。分析同上,左边类型强转为Object,然后右边类型是可以隐式转换为Object的,所以编译通过。(废话,所有对象都他喵的是Object的子类)
  • (Class<? extends Integer>)Integer.class == String.class(Class<? super Integer>)Integer.class == String.class编译报错。都是因为右边类型无法隐式转换为左边的类型。

重写equals一定要重写hashcode

我们重写equals一般是因为不想使用到Object类的最严格的equals方法,因为最严格的equals方法只有当两个引用指向同一个对象才能返回true,但我们重写以后,就可以让equals方法根据对象的某些成员变量来判断相不相等。
在这里插入图片描述
而“重写equals一定要重写hashcode”这句话的使用场景就是我们使用了可哈希的容器,这与哈希容器放置元素的过程有关:

  1. 放置元素时,先调用元素的hashcode方法,为元素找到哈希桶的位置,之后元素就会放到这个哈希桶里。
  2. 此时哈希桶里可能已经有了多个元素。根据上图,两个对象hashcode相同,并不能得出两个对象是否为equal的。而哈希容器是不允许有两个equal的元素同时放在容器里的。所以哈希桶里的每个元素会分别和传入的元素执行equals比较。
  3. 如果哈希桶的每个元素执行equals比较后,返回的都是false,那么放入传入的元素。如果哈希桶里某个元素和传入的元素执行equals比较后,返回了true,那么需要执行相应的策略(1.用传入元素替换掉哈希桶里既存元素 2.忽略传入元素,哈希桶里既存元素不变)。
  4. 这也是为什么源码注释说,unequal的两个对象最好是产生不同的hashcode,因为一个哈希桶里的元素越多,需要执行的equals的次数就越多。

结合以上知识点分析,如果我们重写equals后没有重写hashcode(假设你重写equals后,其逻辑使用对象的成员变量来判断相不相等),将会发生什么:

  • 哈希容器开始放置你的定义的元素,由于你没有重写hashcode,所以只要是不同的对象,就肯定能放入容器中。
  • 由于不同的对象肯定产生不同的hashcode,所以肯定都能放入容器中。而且放置进容器时,也不会再调用你重写的equals方法了,因为各个元素都处于不同的哈希桶里,没有必要再调用equals
  • 因为从来没有调用过equals,所以即使两个对象的成员变量完全一样(因为重写了equals,所以程序员认为是equal的),这两个对象也能同时存在于哈希容器中。

总结一下:重写equals后一定要重写hashcode的原因是,在使用哈希容器的场景下,如果不重写hashcode,那么将会导致重写的equals方法从来不会被调用到。而这将会导致哈希容器里能同时存在两个我们认为是equal的对象。

HashMap的简单实现

上面提到的哈希容器放置元素的过程,可以通过下面这个HashMap的简单实现例子来进一步加深理解。来自Java编程思想——17.9.2为速度而散列——SimpleHashMap类,要想运行此程序,需要导入书中的jar包。

//: containers/SimpleHashMap.java
// A demonstration hashed Map.
import java.util.*;
import net.mindview.util.*;

public class SimpleHashMap<K,V> extends AbstractMap<K,V> {
    // Choose a prime number for the hash table
    // size, to achieve a uniform distribution:
    static final int SIZE = 997;
    // You can't have a physical array of generics,
    // but you can upcast to one:
    @SuppressWarnings("unchecked")
    LinkedList<MapEntry<K,V>>[] buckets =
        new LinkedList[SIZE];
    public V put(K key, V value) {
        V oldValue = null;
        int index = Math.abs(key.hashCode()) % SIZE;//找到哈希桶的位置
        if(buckets[index] == null)
            buckets[index] = new LinkedList<MapEntry<K,V>>();
        LinkedList<MapEntry<K,V>> bucket = buckets[index];
        MapEntry<K,V> pair = new MapEntry<K,V>(key, value);
        boolean found = false;
        ListIterator<MapEntry<K,V>> it = bucket.listIterator();
        while(it.hasNext()) {//在循环中,桶里的每个既存元素需要和传入元素进行equals比较
            MapEntry<K,V> iPair = it.next();
            if(iPair.getKey().equals(key)) {//如果传入元素和桶内元素有重复,那么执行策略是新的替换旧的
                oldValue = iPair.getValue();
                it.set(pair); // Replace old with new
                found = true;
                break;
            }
        }
        if(!found)//如果传入元素和桶内元素没有重复,那么正常添加
            buckets[index].add(pair);
        return oldValue;
    }
    public V get(Object key) {
        int index = Math.abs(key.hashCode()) % SIZE;
        if(buckets[index] == null) return null;
        for(MapEntry<K,V> iPair : buckets[index])//get函数一样,需要桶内每个元素与传入元素进行equals比较
            if(iPair.getKey().equals(key))
                return iPair.getValue();
        return null;
    }
    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> set= new HashSet<Map.Entry<K,V>>();
        for(LinkedList<MapEntry<K,V>> bucket : buckets) {
            if(bucket == null) continue;
            for(MapEntry<K,V> mpair : bucket)
                set.add(mpair);
        }
        return set;
    }
    public static void main(String[] args) {
        SimpleHashMap<String,String> m =
            new SimpleHashMap<String,String>();
        m.putAll(Countries.capitals(25));
        System.out.println(m);
        System.out.println(m.get("ERITREA"));
        System.out.println(m.entrySet());
    }
} /* Output:
{CAMEROON=Yaounde, CONGO=Brazzaville, CHAD=N'djamena, COTE D'IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRICAN REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone, BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul, KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa}
Asmara
[CAMEROON=Yaounde, CONGO=Brazzaville, CHAD=N'djamena, COTE D'IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRICAN REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone, BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul, KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa]
*///:~
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/anlian523/article/details/103229394
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢