equals方法知多少

本文探讨了Java中equals方法的基础知识,包括Object类的默认实现、String和Integer类的重写逻辑。强调了为何需要重写equals以及同时重写hashcode的重要性,并通过分析HashMap源码解释了其背后的原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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源码解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值