咱们永远不晓得面试官为什么能找出这么多奇奇怪怪的场景去比拟两个变量的值,但这也确实是根底中的根底。只是如果不好好的理解分明这块内容,就很有可能在阴沟里翻车,被啪啪打脸。本文就具体的讲述一下,equals、==和hashCode之间的情感纠纷,基本上大部分变量间的比拟都绕不开它们三个。
在浏览本文之前,最好你得彻底的弄懂java中根本类型,尤其是主动拆装箱的场景和常量池之类的。只管我在本篇文章曾经尽量用大白话形容了,然而如果你对根本类型一目了然,会了解的更容易。
想更全面的理解根本数据类型以及包装类,能够先浏览:Java中的根本数据类型
一、==和equals的区别
1.==比拟符:
先说【==】,它的性能就是比拟它两边的值是否雷同。
【==】其实没那么简单,它的性能就只是比拟两边的值是否相等。只是如果变量是援用类型(Integer、String、Object)的话,比拟的就是内存地址,因为援用类型变量存的值就是对象的地址而已。而根本类型(int、double)的变量存的就是它们的值,和内存地址什么的无关。
所以咱们在用【==】比拟援用类型的变量时,就麻烦了。如果援用类型是Integer、String、Long这种,咱们比拟它的时候必定是打算比拟它们代表的值,而不是比拟什么内存地址。
显然,咱们无奈扭转【==】的性能,无奈让它读取援用类型所代表的值去进行比拟,所以equals()就呈现了。
2.equals()办法:
equals()是Object类的办法,而Object类又是所有类的祖宗(父类),所以所有的援用类型都有equals()办法。
首先咱们看Object中的equals()办法:
public boolean equals(Object obj) { return (this == obj); }复制代码
贼xx简略,就只是用了【==】,这时候可能大家就无语了,这不是脱裤子放屁吗,那我特么为啥不间接用==呢?
因为java中是有重写这一说的,如果是咱们本人定义的类,通常不会重写equals()办法。这样的话如果应用了equalst()办法的话,实际上就会调用Object里的这个equals(),和==无异。
但如果是Integer、String这种包装类,它们的源码曾经重写过equals()办法了,重写后的equals()就不是简略的调用【==】了,咱们能够看看Integer的源码:
public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }复制代码
简略剖析下逻辑,首先是判断了要比拟的对象是不是Integer的实例,毕竟只有同类能力比拟内容嘛,如果是不同类型比拟个锤子,先转成同类型再说吧。而后外部获取了该对象的int值。家喻户晓int是根本类型,所以这个equals的实现原理就是取出两个变量的int值而后进行【==】比拟,以达到比拟的是值内容而不是内存地址的成果。
其余的包装类String、Long也是有通过重写的,所以它们的equals办法都是比拟值的内容而不是内存地址。
3.总结==和equals()
如果是根本类型,只能用【==】,它们没有equals办法。
如果是援用类型,间接【==】的话是比拟内存地址。如果这个援用类型重写过equals()办法,能够用equals()办法比拟内容,如Integer、String……等罕用的援用类型都是有重写过euqals()的。
二、int、Integer和new Integer()的区别
说完【==】和equals,咱们还须要理解不同赋值形式也会影响【==】的后果。
1.int、Integer和new Integer()比拟的后果
int无需多说,根本类型,无论是申明变量还是==比拟咱们都很分明了,都只是比拟值而已。
Integer的初始化就不太一样了,认真想想,咱们是不是通常Integer a1=3
这样申明的比拟多呢?然而大家应该都晓得初始化一个对象,该当是Integer a1=new Integer(3)
,因而就会扯出一些问题。
举例:
Integer a1=3; Integer a2=3; Integer a3=new Integer(3); Integer a4=new Integer(3); System.out.println(a1==a2); System.out.println(a3==a4); System.out.println(a1==a3);复制代码
后果是true、false,离谱吧,创立了4个3,后果却不一样。置信还有人会好奇:如果是a1==a3呢,这个后果我也先放进去,是false。
由此能够看出,间接将int值赋值给Integer和new初始化的形式是不同的,所以咱们必须先理解到int值间接赋给Integer变量这种形式非凡在哪里。
2.int值间接赋给Integer变量有什么不同
置信学过包装类的会晓得,间接将int值赋给Integer就是主动装箱,然而不晓得什么是主动装箱也没关系,在这里我先简略的阐明一下,前面能够本人去加深这一块内容的学习。
零碎会主动将赋给Integer变量的int值封装成一个Integer对象,例如Integer a1=3
,实际上的操作是
Integer a = Integer.valueof(3)复制代码
这里有个须要留神的中央,在Integer.valueof()办法源码里,当int的范畴在-128——127之间时,会通过一个IntegerCache缓存来创立Integer对象;当int不在该范畴时,就间接是new Integer()创建对象,所以会有如下状况:
(1)Integer a = 120;Integer b = 120;a == b
为true
(2)Integer a = 130; Integer b = 130;a == b
为false
只不过加了10,后果就齐全不一样了,这就是因为两个Integer变量赋值超过了127,实质上是用了new Integer(),比拟内存当然就不一样了。而缓存范畴内的数据就只是从Integer缓存里取的雷同的值,天然指向的也是雷同的地址。
3.总结int、new Integer()、int间接赋值Integer三种形式的比拟
同样的值,不同的赋值形式,总共分三种状况,只须要记住上面这三种,当前就不会有疑难了:
(1)只有比拟的任意一方有int
只有是和int值比拟,不论另一边是怎么赋值,都会主动拆箱成int类型进行比拟,所以只有有一方是int,【==】比拟的后果就是true。
(2)两个间接赋值Integer之间比拟
因为IntegerCache缓存的缘故,产生了这种状况,当间接赋的值是-128-127之间时,返回为true,因为内存地址本质是雷同的。超出这个范畴就相当于两个new Integer在比拟内存地址,返回false。
(3)剩下的其余状况
剩下的状况就只有两个new Integer()之间、或一个new与一个间接赋值Integer比拟了,这种状况都是比拟内存地址,并且因为至多有一边是new,所以后果都是false。
其余包装类型间接赋值的异同
还能够触类旁通,如果想晓得Long、Double类型的间接赋值外部是什么,也能够查看其valueOf()办法的源码。
例如Long和Integer是一样的,有一个-128~127的缓存,Double和Float则是没有缓存间接返回new。
上面给出几个样例源码:
Integer的valueOf: public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } Double的valueOf: public static Double valueOf(String s) throws NumberFormatException { return new Double(parseDouble(s)); } Boolean的valueOf: public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); }复制代码
三、String间接赋值和new String()比拟的区别
而后String也须要特地的阐明一下,因为它并不属于根本类型,所以没有int、long那种类型,这种状况咱们只须要比拟两种状况,间接赋值和new,也就是比拟:
String a=new String("haha"); String b="haha"; System.out.println(a==b);复制代码
后果是false,因为只有有一边是new的形式初始化的变量,那地址必定是不一样的,并且这里也是用【==】进行比拟地址,天然是false。
字符串常量池
对于String的间接赋值,则须要先阐明一下字符串常量池。
String类是咱们平时我的项目中应用的很多的对象类型,jvm为了晋升性能和缩小内存开销,以及防止字符的反复创立,专门保护了一块字符串常量池的内存空间。当咱们须要应用字符串时,就会先去这个常量池中找,如果存在该字符串,就间接将该字符串的地址返回去间接用。如果不存在,则用new进行初始化并将这个字符串投入常量池中以供前面应用。
这个字符串常量池就是通过间接赋值时应用的,所以如果是间接赋值的形式初始化雷同内容的String,那么其实都是从同一个常量池里取到字符串,地址指向的是同一个对象,天然后果都是雷同的。
说个题外话,通常也是倡议应用间接赋值的形式的,因为这样能够节俭内存开销。
字符串的拼接比拟
还有种非凡的状况,是拼接字符串进行比拟。
举个简略的例子:
String a = "hello"; String d = "helloworld"; System.out.println(d == a + "world"); //false System.out.println(d == "hello" + "world"); //true复制代码
如果只看内容,d都是和helloworld进行了比拟,然而带有变量的就是false,纯字符串的就是true,这是为什么呢?
其实这跟jvm的编译期无关,在进行编译时,它能够辨认出"hello" + "world"这样的字面量和"helloworld"是雷同的,认为这是间接的字面量赋值。通过反编译其实能够看进去,编译后,它间接将"hello" + "world"编译成了"helloworld"。所以天然都是在同一个常量池里找,比拟起来也是雷同的。
而一旦波及了变量,编译时无奈判断这点,也就做不了解决。在启动运行后,就会通过new的形式创立。一旦通过new创立变量,那么地址必定是不同的。
另外,还有一种状况,就是加了关键字final的字符串变量,它会被视为常量,因为是final不可变的:
final String a = "hello"; String d = "helloworld"; System.out.println(d == a + "world"); //true复制代码
这里因为a被视为了常量,所以同样认为是字面量赋值,最终还是在常量池中获取的值,后果就是true了。
总结字符串比拟
如果有任一边是通过new赋值的,那么后果必定是false。
如果两边都是间接赋值的,或是通过final变量进行拼接赋值的,后果是true。只有有一边有波及非final变量,后果就是false。
四、hashCode()和equals()
1.hashCode()介绍
hashCode()办法的作用是获取哈希码,也称为散列码,它实际上只是返回一个int整数。
然而它次要是利用在散列表中,如:HashMap,Hashtable,HashSet,在其余状况下个别没啥用,起因前面会阐明。
2.hashCode()和equals() 的关系
和equal()办法作用相似,hashCode()在java中的也是用于比拟两个对象是否相等。咱们应该都大略听过一句话:重写equals()办法的话肯定要重写hashCode()。然而素来也不晓得是为啥,这里就阐明一下这点。
分两种状况:
①首先一种状况是确定了不会创立散列表的类,咱们不会创立这个类的HashSet、HashMap汇合之类的。这种状况下,这个类的hashCode()和equals() 齐全没有任何关系,当咱们比拟这个类的两个对象时,间接用的就是equals(),齐全用不上hashCode()。天然,也没啥必要重写。
②另一种状况天然就是可能会须要应用到散列表的类了,这里hashCode()和equals()就比拟有关系了:
在散列表的应用中,常常须要大量且疾速的比照,例如你每次插入新的键值对,它都必须和后面所有的键值对进行比拟,确保你没有插入反复的键。这样如果每个都用equals,可想而知,性能是非常可怕的。如果你的equals再简单一些,那就凉凉了。
这时候就须要hashCode()了,如果利用hashCode()进行比照,只有生成一个hash值比拟数字是否雷同就能够了,能显著的提高效率。然而尽管如此,本来的equals()办法还是须要的,hashCode()尽管效率高,可靠性却有余,有时候不同的键值对生成的hashcode也是一样的,这就是哈希抵触。
在应用时,hashCode()将与equals联合应用。尽管hashcode可能会让不同的键产生雷同的hashcode,然而它能确保雷同的对象返回的hashcode肯定是雷同的(除非你重写没写好),咱们只须要利用前面这点,一样能够提高效率。
在散列表中进行比照时,先比拟hashCode(),如果它不相等,那阐明两个对象必定不可能雷同,就能够间接进行下一个比拟了。但如果hashCode()雷同,因为哈希抵触的缘故咱们无奈直接判断两个对象是雷同的,就必须持续比拟equals()来获取牢靠的后果。
注(怕大家看不懂因果逻辑,简略阐明下):因为雷同对象肯定是雷同hashcode,所以雷同对象肯定不会有不同的hashcode,所以两个对象如果是不同的hashcode,那么这两个对象肯定是不同的。
所以如果这个类可能会创立散列表的状况下,重写了equals办法,就必须重写配套的hashcode办法,他们两个在散列表中是搭配应用的。
3.如何重写hashCode办法
外围是保障雷同的对象能返回雷同的hash值,尽量做到不同的对象返回不同的hash值。
这点可难可易,次要能保障外围的规定即可。例如Integer的hashcode就很简略粗犷,间接返回它所代表的的value值。也就是1的hashcode还是1,100的hashcode还是100。
然而这样也是合乎外围规定的:雷同的对象,绝壁是雷同的hashcode。
所以实现起来依照这个规定做,就没问题了。
再夸大点举个例子,哪怕你hashcode固定返回1,不管是谁都返回1,那它也是合乎这个规定的。
只是它没有尽量做到第二个规定:不同的对象返回不同的hash值。
然而它还是能够失常的用,不影响,因为咱们在散列表中不会取到有问题的数据。它因为全部都是雷同的hashcode,所以每次比拟都会比拟到equals而已。只是性能慢了,然而不会有谬误数据,所以能够这样用。
五、对于比拟的一些例子
这里提供一些例子,心愿能让大家产生一种解题思路一样的货色
Integer a = 1; Integer b = 2; Integer c = 3;复制代码
1.c==(a+b)
后果是true,因为a+b必然要主动拆箱,变成int值3,而后Integer和int比拟又会拆箱一次,所以实质上最终是两个int数据3比拟。
Integer d = 2; Integer e = 2; Integer f = 200; Integer g = 200;复制代码
2.d==e和f==g
d==e是true,f==g却是false,首先==的两边都是对象类型,所以不会拆箱,而是比拟内存地址。而因为-128到127有缓存的对象,所以在赋值给d和e主动装箱时调用的不是new构造方法,而是间接读取了缓存里的值为2的对象,并赋给d和e,所以它们是相等的。而200曾经超出了缓存范畴,所以实质上是调用了new Integer()新建了对象,天然内存地址不同。
Long h = 3L; Long i = 2L;复制代码
3.h==(a+b)
后果是true,尽管h是Long类型,但通过a+b的拆箱运算后,实质是Long类型数据3和int根本数据3比拟,而后Long拆箱变成long类型比拟int类型,再依据不同根本类型的主动转换,int转换成long,最初就是两个long的根本数据3L进行比拟了。
4.h.equals(a+b)
后果是false,这里是调用了equals办法,先通过a+b拆箱运算,等价于h.equals(3),通过看equals源码,基本上如果是不同包装类间比拟的话,都是间接返回false,并没有设想中会那么智能的进行转换哦。如果是同类型包装类型,就拆箱后比内容。
5.h.equals(a+i)
后果是true,这里和下面一问不同的是,它外面是Integer+Long,通过拆箱就是long+int,会主动转换为高级的long类型,所以外面实际上是h.equals(3L),是同种包装类型,会拆箱比拟值。
Integer j = new Integer(5); Integer k = new Integer(4); Integer m = new Integer(1);复制代码
6.j==k+m
后果是true,尽管3个都是new的对象,==按理说是比拟地址,必定是不一样的,然而因为k+m波及运算,会主动拆箱计算结果,这样本质上就是j==5,右边是对象左边是根本类型5,这样后果就明了了。
六、总结
一个小小的比拟是否相等,也能搞出这么多门道。这也阐明了根底的重要性,平时咱们在工作中可能就算不懂它们的区别,轻易应用,也能够用的好好的,至多外表不会有什么问题。但这都是潜在的隐患,只有认真学过的人,能力发现这些隐患。
参考:《2020最新Java根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/691874...