深究equals

简介: 在我们做项目的时候经常会将两个对象或者变量作比较,这时候会用到equals或者==,二者的区别在我们初学Java时就已经搞清楚了,一个比较的是值是否相等,另一个比较的是地址是否相等,但是equals真的只是比较两个值是否相等吗?如果是,那应该如何比较呢?

在我们做项目的时候经常会将两个对象或者变量作比较,这时候会用到equals或者==,二者的区别在我们初学Java时就已经搞清楚了,一个比较的是值是否相等,另一个比较的是地址是否相等,但是equals真的只是比较两个值是否相等吗?如果是,那应该如何比较呢?


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。


那么它如何来保证内存地址不重复的呢?



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和hashCode(com.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是一对搭档?他俩到底啥关系需要绑定到一块?


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是做什么的?


先来看一段代码:


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

   }

}



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

   }


目录
相关文章
|
3月前
|
Java
【Java基础面试二十二】、为什么要重写hashCode()和equals()?
这篇文章解释了为什么需要重写`hashCode()`和`equals()`方法:因为Object类的`equals()`默认使用`==`比较,这在业务中通常是不够的,我们需要根据对象内容来比较相等性;同时,为了保持`hashCode()`与`equals()`的联动关系,一旦重写了`equals()`,通常也需要重写`hashCode()`。
【Java基础面试二十二】、为什么要重写hashCode()和equals()?
|
3月前
|
Java
【Java基础面试二十一】、说一说hashCode()和equals()的关系
这篇文章讨论了Java中`hashCode()`和`equals()`方法之间的关系,强调如果两个对象相等,它们必须有相同的哈希码,但有相同哈希码的对象未必相等,并解释了这一关系在HashSet集合中判断元素是否重复的应用场景。
【Java基础面试二十一】、说一说hashCode()和equals()的关系
|
存储 IDE Java
每天一道面试题之==和equals的区别是什么?
每天一道面试题之==和equals的区别是什么?
|
6月前
|
Java
JAVA中比较对象是否相等的方式是什么?为什么重写equals就一定要重写hashcode?百天百题(3/100)
JAVA中比较对象是否相等的方式是什么?为什么重写equals就一定要重写hashcode?
关于==和equals的区别和联系,面试这么回答就可以
关于==和equals的区别和联系,面试这么回答就可以
“==“和equals方法有什么区别(面试)
" == " 比较的是值 " == " 如果比较的是基本数据类型,比较的则是变量值 " == " 如果比较的为引用数据类型,比较的则是地址值
90 0
Java 看一眼equals吧,都用这么久了
Java 看一眼equals吧,都用这么久了
Java 看一眼equals吧,都用这么久了
|
Java
再也不要对java中==和equals的区别有困惑了,这篇文章保证你能懂
想到你应该无数次看到过这个问题了,也可能你已经看过无数篇文章了,如果你还是一头雾水或者是不理解,那试着看看这篇文章,相信你一定能看懂。
118 0
再也不要对java中==和equals的区别有困惑了,这篇文章保证你能懂
|
存储 算法 Java
Java 细品 重写equals方法 和 hashcode 方法
Java 细品 重写equals方法 和 hashcode 方法
279 0
Java 细品 重写equals方法 和 hashcode 方法
|
编译器
为什么不用 == 而用 .equals() 比较字符串?听完后,面试官对我的回答表示赞赏
又到了招聘旺季,鸭哥的读者中,最近应该有不少小伙伴在准备面试吧。
128 0
为什么不用 == 而用 .equals() 比较字符串?听完后,面试官对我的回答表示赞赏