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

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

作者: 博学谷狂野架构师 | 来源:发表于2022-08-02 13:52 被阅读0次
    file

    面试最常问的问题

    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:

    file

    tips: 加载到方法区

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

    2、分配内存空间

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

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

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

    file

    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());
        }
    

    输出如下

    file

    结论:

    很显然,默认的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());
        }
    }
    

    输出如下

    file

    达到了我们的逾期,相同的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)

    file

    目的达到!

    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
    
        }
    }
    
    file

    版本声明:(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源码,总结如下

          本文链接:https://www.haomeiwen.com/subject/itqjwrtx.html