H5W3
当前位置:H5W3 > java > 正文

【Java】从0开始学习变量间的比较:==、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变量有什么不同

系统会自动将赋给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 == btrue

(2)Integer a = 130; Integer b = 130;a == bfalse

只不过加了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()来获取可靠的结果。

所以如果这个类可能会创建散列表的情况下,重写了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…

本文地址:H5W3 » 【Java】从0开始学习变量间的比较:==、equals、hashCode

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址