目录
1. 基础知识
1.1 equals方法作用
当然是判断相等的逻辑
在写代码的时候,常会遇到判断相等的逻辑,一般情况下用的最多的是判断String对象是否相等,如下代码
if(user.equals("于谦")){
System.out.println("抽烟、喝酒、烫头");
}
上面代码是非常简单的,我们再往深层了看看,JDK是如何实现equals逻辑的
1.2 JDK对equals方法的默认实现
1.2.1 先刨祖坟-Object类的实现
Java的经典概念之一就是继承,子类从父类那里获得定义好的能力。因此即使一个很普通的JAVA类,通过继承也会变得异常的强大。
我们定义一个String对象,直接就能点出来equals方法,那么这个方法肯定是继承下来的。
追本溯源,Java类的祖宗就是Object类,所有的类都是继承它的,我们先看看这个类的结构,可以看到equals方法的身影
具体源代码,源码的注释还是非常规范全面的,精简一下表达的意思如下,至于为什么要同时重写hashcode,见下文
可以看到默认的逻辑,就是比较两个对象的内存地址是否相等(不清楚的同学,补充一些JVM的知识)
/**
* 判断其他对象是否等于此对象.也就是判断等价关系
* 需要注意的是,通常情况下重写该方法的时候,一定也要同时重写hashCode方法
* 目的是维护hashCode方法的约定,这个约定的意思就是两个对象equals,那么哈希码也必须相等
*/
public boolean equals(Object obj) {
return (this == obj);
}
1.2.2 String类的实现
String的名字叫作“字符串”,顾名思义,就是字符组成的串,实际上就是对java基本类型中char类型的封装(字符型)。
可以看到源码中有一个成员变量的定义,就是字符型的数组,对String类型的操作都是围绕这个成员变量来的。
/** The value is used for character storage. */
private final char value[];
明白以上的原理后,再看equals的重写
其实逻辑很简单:
首先如果压根两个引用指向的就是同一个对象,那么就不用后续的判断了,直接返回两个字符串相等。
如果两个引用不是同一个对象,那么还需要进一步判断,逻辑就是取出两个字符串中包含的字符数组,逐一比较数组中的每一个字符,如果有一个不相等,那么就认为这两个字符串不相等。否则就是相等。
这里有个技巧可以借鉴,就是当比较两个数组内容的时候,按照常规的想法,可能上来就会建立一个双重循环逐一比对,而大师们写的代码就是不一样,一次循环就搞定了。
/**
* 比较当前的String是否等于指定的String
*/
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
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;
}
按照Object对equals方法重写的约束,如果重写equals方法就要同步重写hashcode方法,保证equals相等的情况下,两个对象的hashcode也是相等的,我们找一下,果然也对hashcode进行了重写。
查看源代码,其实还是把内部的char数组取出来,进行计算最后得到hashcode值,因此只要char数组中的字符一直,那么hashcode也一定是一致的,符合逻辑,OK!
/**
* 返回String的hashCode值
* 计算公式如下
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
*/
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;
}
1.2.3 Integer包装类的实现
Integer是对int基本类型的封装
相等的逻辑,其实就是把Integer中的int变量的值拿出来进行一致性判断,很简单,就不多说了
/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is an {@code Integer} object that
* contains the same {@code int} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
而同样,对hashcode方法也进行了重写
无非就是把int值取出来返回来,因此也保证了逻辑要求.
/**
* Returns a hash code for this {@code Integer}.
*
* @return a hash code value for this object, equal to the
* primitive {@code int} value represented by this
* {@code Integer} object.
*/
@Override
public int hashCode() {
return Integer.hashCode(value);
}
/**
* Returns a hash code for a {@code int} value; compatible with
* {@code Integer.hashCode()}.
*
* @param value the value to hash
* @since 1.8
*
* @return a hash code value for a {@code int} value.
*/
public static int hashCode(int value) {
return value;
}
其他封装类大家有时间也可以看看,大同小异。
2. 进一步思考
2.1 为什么要重写equals
在上一章节中,可以看到String、Integer类都重写了equals方法,为什么呢?
因为出于业务需要,如果String不重写eqauls,按照Object类对eqauls逻辑的定义,当比较两个String对象的时候,会比较两个引用是否指向同一块内存地址,即是否为同一个对象。如果是同一个,那么就相等,否则就不等。
而这个逻辑对于String来讲是行不通的,因为我们只想知道两个String对象代表的字符串内容是否一致,只要内容一致就认为两个String对象相等,因此是需要重写equals方法的。
Integer对象同理。
2.2 为什么还要同时重写hashcode方法
前面在查看Object的equals方法的时候,注意到,JDK对于hashcode的约束,“两个对象equals返回true,那么哈希码也必须相等”。
为什么jdk会建立这样的约束呢?我们还需要进一步查看源码。
2.2.1 在HashMap源码中的体现
下面是HashMap的源码片段
对于HashMap的源码解析有时间我再单独写一篇博客分析,在这里大家只要大概知道它的架构即可,主要有下面的知识点(简单知道一下,看不懂也没关系):
- HashMap底层结构是,数组+链表+红黑树构成的
- 数组:内部定义了一个Node类型(自定义内部类,包含Key、Value属性)的数组,数据都是存储在这个容器中的
- 链表:当执行put操作的时候,如果出现hash冲突,那么就会创建链表来存储所有的冲突项
- 红黑树:为了提高性能,方式链表过长导致性能下降,当链表长度达到一定程度,就会转化为红黑树
我们看一下使用非常频繁的put方法源码
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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)
n = (tab = resize()).length;
//② 如果目标位置为空,创建Node,放入这个位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//③ 判断新增数据与目标数据是否相同,这里是重点,判断相同的标准就是key的hash值与eqauls同时相等的时候,才会认为是同一条数据
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//④ 如果是红黑树的处理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//⑤ 链表的处理
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
使用重载定义了两个不同参数的put方法,核心方法是第二个
源码中③中解释了我们要找寻的问题,当Key为对象类型的时候,会同时使用equals和hash码判断放入对象与已存在对象是否为同一对象,如果这两者不一致,数据的存储和我们的预期就不一致了。
关于HashMap的源码解析,请见我的另一篇博客HashMap源码解析