草庐IT

【面经】被虐了之后,我翻烂了equals源码,总结如下

博学谷狂野架构师 2023-04-20 原文

面试最常问的问题

1、equals比较的什么?

2、有没有重写过equals?

3、有没有重写过hashCode?

4、什么情况下需要重写equals()和hashCode()?

1) equals源码

目标:如果不做任何处理(可能绝大大大多数场景的对象都是这样的),jvm对同一个对象的判断逻辑是怎样的

我们先读一下Object里的源码:

    /**
     * Indicates whether some other object is "equal to" this one.
     * <p>
     * The {@code equals} method implements an equivalence relation
     * on non-null object references:
     * <ul>
     * <li>It is <i>reflexive</i>: for any non-null reference value
     *     {@code x}, {@code x.equals(x)} should return
     *     {@code true}.
     * <li>It is <i>symmetric</i>: for any non-null reference values
     *     {@code x} and {@code y}, {@code x.equals(y)}
     *     should return {@code true} if and only if
     *     {@code y.equals(x)} returns {@code true}.
     * <li>It is <i>transitive</i>: for any non-null reference values
     *     {@code x}, {@code y}, and {@code z}, if
     *     {@code x.equals(y)} returns {@code true} and
     *     {@code y.equals(z)} returns {@code true}, then
     *     {@code x.equals(z)} should return {@code true}.
     * <li>It is <i>consistent</i>: for any non-null reference values
     *     {@code x} and {@code y}, multiple invocations of
     *     {@code x.equals(y)} consistently return {@code true}
     *     or consistently return {@code false}, provided no
     *     information used in {@code equals} comparisons on the
     *     objects is modified.
     * <li>For any non-null reference value {@code x},
     *     {@code x.equals(null)} should return {@code false}.
     * </ul>
     * <p>
     * 该方法用于识别两个对象之间的相似性
     * 也就是说,对于一个非null值,x和y,当且仅当它们指向同一个对象时才会返回true
     * 言外之意,和==没啥两样。
     * The {@code equals} method for class {@code Object} implements
     * the most discriminating possible equivalence relation on objects;
     * that is, for any non-null reference values {@code x} and
     * {@code y}, this method returns {@code true} if and only
     * if {@code x} and {@code y} refer to the same object
     * ({@code x == y} has the value {@code true}).
     * <p>
     * Note that it is generally necessary to override the {@code hashCode}
     * method whenever this method is overridden, so as to maintain the
     * general contract for the {@code hashCode} method, which states
     * that equal objects must have equal hash codes.
     *
     * @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);
    }

猜想:如果我们不做任何操作,equals将继承object的方法,那么它和==也没啥区别!

下面一起做个面试题,验证一下这个猜想:

package com.eq;

import java.io.InputStream;

public class DefaultEq {
    String name;
    public DefaultEq(String name){
        this.name = name;
    }
    public static void main(String[] args) {
        DefaultEq eq1 = new DefaultEq("张三");
        DefaultEq eq2 = new DefaultEq("张三");
        DefaultEq eq3 = eq1;

        //虽然俩对象外面看起来一样,eq和==都不行
        //因为我们没有改写equals,它使用默认object的,也就是内存地址
        System.out.println(eq1.equals(eq2));
        System.out.println(eq1 == eq2);

        System.out.println("----");
        //1和3是同一个引用
        System.out.println(eq1.equals(eq3));
        System.out.println(eq1 == eq3);

        System.out.println("===");
        //以上是对象,再来看基本类型
        int i1 = 1;
        Integer i2 = 1;
        Integer i = new Integer(1);
        Integer j = new Integer(1);

        Integer k = new Integer(2);

        //只要是基本类型,不管值还是包装成对象,都是直接比较大小
        System.out.println(i.equals(i1));  //比较的是值
        System.out.println(i==i1);  //拆箱 ,
        // 封装对象i被拆箱,变为值比较,1==1成立
        //相当于 System.out.println(1==1);

        System.out.println(i.equals(j));  //
        System.out.println(i==j);   //  比较的是地址,这是俩对象

        System.out.println(i2 == i); // i2在常量池里,i在堆里,地址不一样

        System.out.println(i.equals(k));  //1和2,不解释
    }
}


结论:

  • “==”比较的是什么?

    用于基本数据(8种)类型(或包装类型)相互比较,比较二者的值是否相等。

    用于引用数据(类、接口、数组)类型相互比较,比较二者地址是否相等。

  • equals比较的什么?

    默认情况下,所有对象继承Object,而Object的equals比较的就是内存地址

    所以默认情况下,这俩没啥区别

2) 内存地址生成与比较

tips:既然没区别,那我们看一下,内存地址到底是个啥玩意

目标:内存地址是如何来的?

Main.java

    public static void main(String[] args) {
        User  user1=new User("张三");
        User  user2=new User("张三");
    }

1、加载过程(回顾)

从java文件到jvm:

tips: 加载到方法区

这个阶段只是User类的信息进入方法区,还没有为两个user来分配内存

2、分配内存空间

在main线程执行阶段,指针碰撞(连续内存空间时),或者空闲列表(不连续空间)方式开辟一块堆内存

每次new一个,开辟一块,所以两个new之间肯定不是相同地址,哪怕你new的都是同一个类型的class。

那么它如何来保证内存地址不重复的呢?(cas画图)

3、指向

在栈中创建两个局部变量 user1,user2,指向堆里的内存

归根到底,上面的==比较的是两个对象的堆内存地址,也就是栈中局部变量表里存储的值。

public boolean equals(Object obj) {
    return (this == obj);//本类比较的是内存地址(引用)
}

3) 默认equals的问题

需求(or 目标):user1和user2,如果name一样我们就认为是同一个人;如何处理?

tips:

面试最常问的问题

1、equals比较的什么?

2、有没有重写过equals?

3、有没有重写过hashCode?

4、什么情况下需要重写equals()和hashCode()?

1、先拿User下手,看看它的默认行为com.eq.EqualsObjTest

    public static void main(String[] args) {
       //需求::user1和user2,在现实生活中是一个人;如何判定是一个人(相等)
        User user1 = new User("张三");
        User user2 = new User("张三");
        System.out.println("是否同一个人:"+user1.equals(user2));
        System.out.println("内存地址相等:"+String.valueOf(user1 == user2));//内存地址
        System.out.println("user1的hashCode为>>>>" + user1.hashCode());
        System.out.println("user2的hashCode为>>>>" + user2.hashCode());
    }

输出如下

结论:

很显然,默认的User继承了Object的方法,而object,根据上面的源码分析我们知道,equals就是内存地址。

而你两次new User,不管name怎么一致,内存分配,肯定不是同一个地址!

怎么破?

2、同样的场景,我们把用户名从User换成单纯的字符串试试com.eq.EqualsStrTest

 public static void main(String[] args) {
        String str1 = "张三";//常量池
        String str2 = new String("张三");//堆中
        String str3 = new String("张三");//堆中
        System.out.println("是否同一人:"+str1.equals(str2));//这个地方为什么相等呢,重写
        System.out.println("是否同一人:"+str2.equals(str3));//这个地方为什么相等呢,重写
        //如果相等,hashcode必须相等,重写
        System.out.println("str1的hashCode为>>" + str1.hashCode());
        System.out.println("str2的hashCode为>>" + str2.hashCode());
    }
}

输出如下

达到了我们的逾期,相同的name,被判定为同一个人,为什么呢?往下看!

String的源码分析

    /**
     * Compares this string to the specified object.  The result is {@code
     * true} if and only if the argument is not {@code null} and is a {@code
     * String} object that represents the same sequence of characters as this
     * object.
     *
     * @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) {
      	//如果内存地址相等,那必须equal
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
          	//如果对象是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) {
                  	//从前往后,任意一个字符不匹配,直接返回false
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
              	//全部匹配结束,返回true
                return true;
            }
        }
        return false;
    }

结论:

String类型改写了equals方法,没有使用Object的默认实现

它不管你是不是同一个内存地址,只要俩字符串里的字符都匹配上,那么equals就认为它是true

3、据此,我们参照String,来重写User的equals和hashCodecom.eq.User2

    @Override
    public boolean equals(Object o) {
      	//注意这些额外的判断类操作
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;
        //比较值
        return name != null ? name.equals(user.name) : user.name == null;
    }

    @Override
    public int hashCode() {
        //返回值的hashCode
        return name != null ? name.hashCode() : 0;
    }

换成User2 再来跑试试 (参考 com.eq.EqualsObjTest2)


目的达到!

4)hashCode与equals

为什么说hashCode和equals是一对搭档?他俩到底啥关系需要绑定到一块?

看代码说话:(com.eq.Contains)

package com.eq;

import java.util.HashSet;
import java.util.Set;

public class Contains {
    public static void main(String[] args) {
        User user1 = new User("张三");
        User user2 = new User("张三");
        Set set = new HashSet();
        set.add(user1);
        System.out.println(set.contains(user2));


        User2 user3 = new User2("张三");
        User2 user4 = new User2("张三");
        Set set2 = new HashSet();
        set2.add(user3);
        System.out.println(set2.contains(user4));
    }
}

结论:

hashCode是给java集合类的一些动作提供支撑,来判断俩对象“是否是同一个”的标准

equals是给你编码时判断用的,所以,这俩必须保持一致的逻辑。

5)总结

1、特殊业务需求需要重写,比如上面的

2、例如map,key放自定义对象也需要重写

3、重写equals后必须要重写hashCode,要保持逻辑上的一致!

1.2.5 关于双等(扩展)

equals被重写后,双等还留着干啥用?

1)String的特殊性

tips:面试常问的问题

intern是做什么的?

先来看一段代码:(com.eq.Intern)

public class Intern {
    public static void main(String[] args) {
        String str1 = "张三";//常量池
        String str2 = new String("张三");//堆中

        //intern;内存地址是否相等(面试常问)
        System.out.println("str1与str2是否相等>>" +(str1==str2));  // false
        System.out.println("str1与str2是否相等>>" +(str1==str2.intern()));  // true

    }
}

版本声明:(JDK1.8)

new String是在堆上创建字符串对象。
当调用 intern() 方法时,
JVM会将字符串添加(堆引用指向常量池)到常量池中

注意:

1、1.8版本只是将hello word在堆中的引用指向常量池,之前的版本是把hello word复制到常量池

2、堆(字符串常量值) 方法区(运行时常量池)不要搞反了

2)valueOf里的秘密

关于双等号地址问题,除了String.intern() , 在基础类型里,如Integer,Long等同样有一个方法:valueOf需要注意

我们先来看一个小例子: 猜一猜结果?

package com.eq;

public class Valueof {
    public static void main(String[] args) {
        System.out.println( Integer.valueOf(127) == Integer.valueOf(127));
        System.out.println( Integer.valueOf(128) == Integer.valueOf(128));
    }
}

奇怪的结果……

源码分析(以Integer为例子):

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * !在-128 到 127 之间会被cache,同一个地址下,超出后返回new对象!
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

本文由传智教育博学谷 - 狂野架构师教研团队发布
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力
转载请注明出处!

有关【面经】被虐了之后,我翻烂了equals源码,总结如下的更多相关文章

  1. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  2. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  3. Ruby on Rails regexp equals-tilde 与 array include 用于检查选项列表 - 2

    我正在使用Rails3.2.3和Ruby1.9.3p0。我发现我经常需要确定某个字符串是否出现在选项列表中。看来我可以使用Ruby数组.includemethod:或正则表达式equals-tildematchshorthand用竖线分隔选项:就性能而言,一个比另一个好吗?还有更好的方法吗? 最佳答案 总结:Array#include?包含String元素,在接受和拒绝输入时均胜出,对于您的示例只有三个可接受的值。对于要检查的更大的集合,看起来Set#include?和String元素可能会获胜。如何测试我们应该根据经验对此进行测试

  4. elasticsearch源码关于TransportSearchAction【阶段三】 - 2

    1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>

  5. (附源码)vue3.0+.NET6实现聊天室(实时聊天SignalR) - 2

    参考文章搭建文章gitte源码在线体验可以注册两个号来测试演示图:一.整体介绍  介绍SignalR一种通讯模型Hub(中心模型,或者叫集线器模型),调用这个模型写好的方法,去发送消息。  内容有:    ①:Hub模型的方法介绍    ②:服务器端代码介绍    ③:前端vue3安装并调用后端方法    ④:聊天室样例整体流程:1、进入网站->调用连接SignalR的方法2、与好友发送消息->调用SignalR的自定义方法 前端通过,signalR内置方法.invoke()  去请求接口3、监听接受方法(渲染消息)通过new signalR.HubConnectionBuilder().on

  6. Simulink方法总结和避坑指南(一)——Simulink入门与基本调试方法 - 2

    文章目录一、项目场景二、基本模块原理与调试方法分析——信源部分:三、信号处理部分和显示部分:四、基本的通信链路搭建:四、特殊模块:interpretedMATLABfunction:五、总结和坑点提醒一、项目场景  最近一个任务是使用simulink搭建一个MIMO串扰消除的链路,并用实际收到的数据进行测试,在搭建的过程中也遇到了不少的问题(当然这比vivado里面的debug好不知道多少倍)。准备趁着这个机会,先以一个很基本的通信链路对simulink基础和相关的debug方法进行总结。  在本篇中,主要记录simulink的基本原理和基本的SISO通信传输链路(QPSK方式),计划在下篇记

  7. ruby - IRB - Ruby 1.9.x 哈希语法 : {if: true} is not equal to {:if => true} - 2

    长话短说,我正在编写一个包含选项参数的方法,如果键的值:if评估为真,该方法将执行某些操作。当我使用新语法在IRB中尝试哈希时,我在IRB中遇到语法错误,提示保持打开状态:1.9.3p374:010>{if:true}1.9.3p374:011?>使用旧语法,效果很好:1.9.3p374:011>{:if=>true}=>{:if=>true}开始语句的所有关键字都表现出相同的行为。例如。def,do,module,case出现在中间和class中的其他保留字可以正常工作:else、end我的问题是:这是预期的行为、错误还是限制? 最佳答案

  8. Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理) - 2

    快速导航(持续更新中…)Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理)Cesium源码解析二(metadataAvailability的含义)Cesium源码解析三(metadata元数据拓展中行列号的分块规则解析)Cesium源码解析四(Quantized-Mesh(.terrain)格式文件在CesiumJS和UE中加载情况的对比)目录1.前言2.本篇的由来3.terrain文件的加载3.1更新环境3.2更新和执行渲染命令3.3数据优化3.4结束当前帧4.总结1.前言  目前市场上三维比较火的实现方案主要有两种,b/s的方案主要是Cesium,c/s的方案主要是u

  9. Ruby 设置类 : equality of sets - 2

    根据RubySet类的文档,“==如果两个集合相等,则返回true。每对元素的相等性根据Object#eql?定义。可以使用Date对象来演示其本质,其中包含不同Date对象但具有相同日期的集合比较相等:require'set'd1=Date.today#=>Thu,30Sep2010putsd1.object_id#=>2211539680d2=Date.today+1#=>Fri,01Oct2010putsd2.object_id#=>2211522320set1=Set.new([d1,d2])d11=Date.today#=>Thu,30Sep2010putsd11.objec

  10. Ruby URI - 如何在 URL 之后获取完整路径 - 2

    给定以下内容,如何获取URL的完整路径uri=URI("http://foo.com/posts?id=30&limit=5#time=1305298413")我只想要posts?id=30&limit=5#time=1305298413我试过uri.path并返回/posts和ui.query返回'id=30&limit=5' 最佳答案 您要找的方法是request_uriuri.request_uri=>"/posts?id=30&limit=5"如果需要,您可以使用任何您想要删除前导/的方法。编辑:要获取#符号后的部分,请使用

随机推荐