共计 7558 个字符,预计需要花费 19 分钟才能阅读完成。
咱们永远不晓得面试官为什么能找出这么多奇奇怪怪的场景去比拟两个变量的值,但这也确实是根底中的根底。只是如果不好好的理解分明这块内容,就很有可能在阴沟里翻车,被啪啪打脸。
本文就具体的讲述一下,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…