JAVA研发三年了,你应该学习JAVA源码了

深入学习equals源码

最近在面试许多工作3-5年应聘者的时候,发现了许多人都没有阅读过String源码,尤其是equals和==的区别的问题以及停留在表层听说层次,没有深入理解。

首先,我们看一下Java的最顶级的基类Object的源码。该源码中包含了12个方法,那我们在开发的过程中常用的方法有5个,线程类中常用的有5个。剩下的finalize已经在jdk9中被标记deprecated,registerNatives是加载本地方法用的,用C语言开发的,咱们也不常用。

JAVA研发三年了,你应该学习JAVA源码了

equals源码分析

     /**
     * @param   obj   the reference object with which to compare.
     * @return  {@code true} if this object is the same as the obj
     *          argument; {@code false} otherwise.
     * @see     #hashCode()
     * @see     java.util.HashMap
     */
    public boolean equals(Object obj) {
        return (this == obj);
    }

上面简单的三行代码就是Object类中equals的源码。主要的比较是两个对象的地址。可以看到,方法体中equals比较其实也是“==”实现的。因此,下次在面试的过程中,你遇见问equals与“==”的区别的时候,你先给出结论。==是比较两个对象地址的,没有重写equals方法的实体类使用equals也是比较地址的。

那么我们经常用String中equals为什么是比较值的呢?接下来我们看一下String类中equals的源码

String中equals方法源码分析

JAVA研发三年了,你应该学习JAVA源码了
	   /** 
     *@param  anObject
     *         The object to compare this {@code String} against
     *
     * @return  {@code true} if the given object represents a {@code String}
     *          equivalent to this string, {@code false} otherwise
     *
     * @see  #compareTo(String)
     * @see  #equalsIgnoreCase(String)
     */
    public boolean equals(Object anObject) {
      //可以看到,String中重新的equals方法在第一步就进行了地址比较,
      //如果两个字符串的地址相等,那么他们的值一定相等,就不用去进行下面的值比较了
        if (this == anObject) {
            return true;
        }
      //这个if表示,如果equals的方法体中不是一个String类型,那么也就直接返回为false
      //比如“蜜蜂攻城狮
        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;
    }

在我们实际编写代码的时候,特别是一些需要重写equals方法的实体类的时候,那么我们需要进行equals的方法重写,重写equals方法需要遵守如下约定。翻译如下

JAVA研发三年了,你应该学习JAVA源码了

(1)自反性:x.equals(x)必须返回true。

(2)对称性:x.equals(y)与y.equals(x)的返回值必须相等。

(3)传递性:x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true。

(4)一致性:如果对象x和y在equals()中使用的信息都没有改变,那么x.equals(y)值始终不变。

(5)非null:x不是null,y为null,则x.equals(y)必须为false。

String类中hashCode方法源码

这就涉及到我们上述在Obejct中看到的另一个方法。细心的朋友可能发现了,hashCode方法的修饰符是native。在Object类中有7个方法都是native修饰的。而在String类中hashCode源码如下。可以看到。该hashCode方法返回的是一个整形。它主要计算的是一个字符串的hash值然后将其缓存从而提高其性能,而计算的算法在注释中有提。那就是s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1],算法中31是因为JVM对其进行了优化处理,即:31 * i == (i << 5) – i

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

重写equals必须重写hashCode的原因

重写hashCode的原则如下,也是我们必须遵守的,就等同于重写equals方法一样也需要遵守原则

JAVA研发三年了,你应该学习JAVA源码了

(1)如果重写了equals()方法,检查条件“两个对象使用equals()方法判断为相等,则hashCode()方法也应该相等”是否成立,如果不成立,则重写hashCode ()方法。

(2)hashCode()方法不能太过简单,否则哈希冲突过多。

(3)hashCode()方法不能太过复杂,否则计算复杂度过高,影响性能。

其实,简单的来说。在我们重写hashCode方法的时候,我们一般会根据自身的业务来进行hashCode算法编写。正如String方法中的hashCode计算算法一样。我们根据业务涉及出自己所需要的算法。

那为什么我们重写了equals方法就需要重写hashCode方法呢?

其实有些人说的,重写了equals方法就一定要重新hashCode。这句话其实是错的

因为hashCode和equals没有必然的区别。因为如果你不需要使用你定义的对象进行散列存储,比如使用hashMap,hashSet等集合,你不重写也没关系。因为hashCode本身是用来根据算法计算对象的散列值,然后根据这个值来决定存放位置。

JAVA研发三年了,你应该学习JAVA源码了

举个简单的例子,如果使用HashMap,那么要保证key唯一,也就是要让其不重复,在java中比较是否相等用equals吧,所以使用equals去进行挨个比较,如果容器中已经存放了多个Key,是不是就需要比较很多次呢?如果有了hashCode值,是不是就简单多了。其实可以看一下hashmap的关于key比较的源码,就一清二楚了。在HashMap源码的625行putVal方法中可以看到634行及以后的内容,hashMap在put值的时候,其实首先用==去比较的是key的hash值。然后再用equals方法。

if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

最后,也是最重要的一点,如果你重写了equals方法,在set,或hashmap容器中使用的时候,两个相等的对象有可能有不同的hashCode,这样就会导致容器中存放两个相同的对象,导致调用容器的方法出现逻辑错误。导致你的equals方法写了也白写,没有起到根本的作用。

又因为容器在我们日常开发中经常用到,因此才有了重写equals建议重写hashCode。也只是建议!

声明:本文由会火号官方原创,如若转载,请注明出处:https://www.huihuohao.com/s/2257.html

发表评论

登录后才能评论