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

深入学习equals源码

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

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

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

equals源码分析

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

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

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

String中equals方法源码分析

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

<code>\t   /** 
*@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;
}/<code>

在我们实际编写代码的时候,特别是一些需要重写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

<code>    /**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
*

* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
*

* using {@code int} arithmetic, where {@code s[i]} is the
* ith 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;
}/<code>

重写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方法。

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

/<code>

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

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


分享到:


相關文章: