hashCode和equals

  1. hashCode和equals
    1. equals
    2. hashCode

hashCode和equals

hashCode和equals,hashMap相关源码解读

equals

在初学java的时候,我们可能会被告知在java中使用 == 是地址比较,使用 equals 是值比较,那么为什么是这样的呢?

我们先看看equals方法,equals方法是来自Object类中的一个方法,也就是说所有的类都有这个方法。

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

可以看见,默认的equals方法实现用的也是 ==,也就是说如果我们创建了一个类,如果不重写equals方法,那么实际上我们使用的还是 ==,也就是地址比较。

地址比较

在java中每个对象引用都指向对象存在于JVM堆中的内存地址,也就是不同的两个实例化对象的地址也是不相同的。

public class Test {

    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        System.out.println(o1 == o2); //false
        System.out.println(o1.equals(o2));//false
    }

}

那么为什么说equals就是值比较呢?其实在java中,在定义类时是推荐我们去重写equalshashCode方法的。

equals方法其实是让用户去自己定义类的比较方法具体实现,举个例子,在下面的代码中两个Integer对象直接使用 == 比较是不同的说明它们的地址不同,是堆中的两个对象。

使用equals方法则返回的是true,那么我们可以肯定的是Integer类一定是重写了equals方法,不再是进行地址比较。

public class Test {

    public static void main(String[] args) {
        //这里我们显式的new两个不同的Integer对象进行比较
        Integer i1 = new Integer(0);
        Integer i2 = new Integer(0);

        System.out.println(i1 == i2); //false
        System.out.println(i1.equals(i2)); //true

    }

}

实际上阅读源码我们可以知道,Integer使用equals比较比较的不再是对象地址,而是包装类的内部静态属性value,这是装饰器模式的很好实践。

private final int value;

public int intValue() {
    return value;
}

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

也就是equals方法实际上是开放给用户实现的,让用户自己去定义两个类是否相等的比较逻辑,如果我们在设计Integer类时,也一定是按照常用的逻辑去比较整型的值,而不是去比较地址,这对于整型equals的具体实现是没有意义的。

那么为什么常说如果要实现equals方法就要一起把hashCode方法也实现了?

hashCode

hashCode方法也是Object中定义的一个方法,使用hash算法来生成对象的哈希码值,是根据对象的内存地址生成的。

返回对象的哈希代码值。支持此方法是为了使用哈希表,例如HashMap提供的哈希表hashCode的一般约定是:每当在Java应用程序的执行过程中对同一对象多次调用时,hashCode方法必须始终返回相同的整数,前提是在对象的equals比较中使用的信息未被修改。

  • 上面是摘录来自JDK的源码注释,简单来说就是只要equals方法比较的结果是相同的,那么对于的hashCode方法生成当前对象的哈希值也需要是相同的。

Integer类中hashCode的实现就是直接返回当前整型类所表示的整型值value,因此equals方法和hashCode方法所使用的都是同一个value值,所以只要value值不变,两个代表不同整数的IntegerhashCode值也是相同的。

public class Test {

    public static void main(String[] args) {
        //这里我们显式的new两个不同的Integer对象进行比较
        Integer i1 = new Integer(0);
        Integer i2 = new Integer(0);

        System.out.println(i1.hashCode());//0
        System.out.println(i2.hashCode());//0

    }

}

源码中还提到hashCode是为了支持hashMap使用的,那么我们再来看看hashMap中是如何使用hashCode

我们知道HashMap底层存放数据的数据结构其实是链表/红黑树,也就是最后都是存在某个Node上的,我们看下hashMapNode的相关属性

  • hash 这个是该Node的key的hash运算后的值
  • key 这个就是key所代表对象的引用
  • value 这个是value所代表对象的引用
  • next 这个是存储该节点的下一个节点
Node(int hash, K key, V value, Node<K,V> next) {
    this.hash = hash;
    this.key = key;
    this.value = value;
    this.next = next;
}

我们只关注hashCode是如何使用的,也就是我们具体看hash这个属性的作用

追踪HashMap.put(key,value)方法

这是hashMapput方法生成新节点的代码,它需要提供一个hash值,根据源码可知该hash值是hashMap中静态hash方法对keyhash运算后得到的结果。

put方法入口

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

hashMap中的hash方法

本质上还是调用了keyhashCode方法,然后又做了一次hash运算

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

具体的putVal实现算法,省略了其它代码

这里我们只看newNode构造方法,因此我们可以确定的是Node中的hash值就是由key对象的hashCode方法提供的

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
                    //....
        if ((p = tab[i = (n - 1) & hash]) == null)
                     //....
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                         //....
            else if (p instanceof TreeNode)
                        //....
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);  //定位到生成Node对象
                                //....
                    }
                             //....
                }
            }
                     //....
        }
         //....
        return null;
    }
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

追踪HashMap.get(key)方法

那么get方法是如何使用hashCode的,我们知道get方法其实就是拿到key对应的value对象,那么hashMap是如何确定外界传入的key值和value所在Node的key值是相同的

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这里是具体的getNode算法实现,我们直接定位最核心的源码部分

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

就是下面这条语句

if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;

我们可以看到,比较外界key值和内部Node的key值,首先是这一句first.hash == hash,比较的是当前节点的hash属性与外界传入key的hash值是否相同,也就是说hashMap判断两个对象是否相同,首先会判断hash值是否相同如果hash值相同则会直接返回该Node。

  • 首先看的是该对象hashCode()方法返回的值是否是相同的。

  • 如果hashCode()不相同,则无需进行进一步判断

  • 如果相同,才会比较对象的地址或满足对象的equals方法也被视为相同的对象

由此我们回到刚才的问题为什么实现equals也要实现hashCode

还是刚才的Integer为例,我们假设Integer类只实现了equals方法而没有实现hashCode方法,那么在hashMap中判断两个Integer对象是否相同使用的就是Object类中的hashCode方法也就是地址。

这样子就导致不管我们equals方法如何定义,只要对象不同,hashMap就不会调用equals方法,这和我们实现equals方法的初衷是违背的。

所以一句话来说就是,hashCode方法是hashMap判断对象是否相同的首要依据,因此如果我们想让hashMap等集合按照我们的预期正常的运作,我们实现equals方法时也要记得实现hashCode方法。

下面是实现hashCodeequals方法的例子以及实现equals但未实现hashCode的例子

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Test {
    
    public static void main(String[] args) {

        //1.实现hashCode和equals方法的例子
        //业务上我们视e1和e2是相同的
        Example1 e1 = new Example1("例1");
        Example1 e2 = new Example1("例1");
        System.out.println(e1.equals(e2));//true

        Map<Example1,Example1> hashMap = new HashMap<>();
        hashMap.put(e1,e1);
        System.out.println(hashMap.get(e2));


        //2.未实现hashCode方法的例子
        //业务上我们视e3和e4是相同的
        Example2 e3 = new Example2("例2");
        Example2 e4 = new Example2("例2");
        System.out.println(e1.equals(e2));//true

        Map<Example2,Example2> hashMap2 = new HashMap<>();
        hashMap2.put(e3,e3);
        System.out.println(hashMap2.get(e4));//null
    }

}

class Example1{

    private String id;

    public Example1(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Example1 example1 = (Example1) o;
        return Objects.equals(id, example1.id);
    }

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

    @Override
    public String toString() {
        return "Example1{" +
                "id='" + id + '\'' +
                '}';
    }
}

class Example2{

    private String id;

    public Example2(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Example2 example2 = (Example2) o;
        return Objects.equals(id, example2.id);
    }

    @Override
    public String toString() {
        return "Example2{" +
                "id='" + id + '\'' +
                '}';
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com

文章标题:hashCode和equals

字数:2.4k

本文作者:Os467

发布时间:2024-05-25, 00:06:13

最后更新:2024-05-25, 00:07:10

原始链接:https://os467.github.io/2024/05/25/hashCode%E5%92%8Cequals/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏