3

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

 2 years ago
source link: https://blog.51cto.com/boxuegu/5536161
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

原创

博学谷狂野架构师 2022-08-02 13:51:50 博主文章分类:技术干货 ©著作权

文章标签 面试 java 后端 jdk源码 文章分类 Java 编程语言 阅读数303

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

面试最常问的问题

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:

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

tips: 加载到方法区

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

2、分配内存空间

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

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

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

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

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());
}
【面经】被虐了之后,我翻烂了equals源码,总结如下_面试_04

很显然,默认的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());
}
}
【面经】被虐了之后,我翻烂了equals源码,总结如下_jdk源码_05

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

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

目的达到!

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是给你编码时判断用的,所以,这俩必须保持一致的逻辑。

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

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

版本声明:(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);
}

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK