一、前言:
equals和==和hashcode是java中的基础中的基础,但是确实容易被问到,而且因为工作中经常用所以很容易在复习时遗漏掉,这几天面了个大厂就在这个问题上翻车了,不仅记混了,而且很坚信自己记混的答案,好在其他问题答的不错,加上面试官容忍度高放过一马,所以今天也就详细整理了下它们三个的恩怨情仇
二、【==】双等号
【==】其实没那么复杂,它的功能就只是比较两边的值是否相等。只是如果变量是引用类型(Integer、String、Object)的话,比较的就是内存地址,因为引用类型变量存的值就是对象的地址而已。而基本类型(int、double)的变量存的就是它们的值,和内存地址什么的无关。
所以我们在用【==】比较引用类型的变量时,就麻烦了。如果引用类型是Integer、String、Long这种,我们比较它的时候肯定是打算比较它们代表的值,而不是比较什么内存地址。
三、equals方法:
equals()是Object类的方法,而Object类又是所有类的祖宗(父类),所以所有的引用类型都有equals()方法。但是有很多类都会重新equals以及hashcode方法
1.object类的equals
先看下object的equals方法
public boolean equals(Object obj) {
return (this == obj);
}
object类的equals方法很简单其实就是【==】
2.Integer类的equals
可以看出Integer类的equals方法其实时比较的值
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
3.String类的equals方法
public boolean equals(Object anObject) {
//判断引用
if (this == anObject) {
return true;
}
//判断是否是String
if (anObject instanceof 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) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
大概流程是:
(1)判断引用是否相等
(2)判断是否是String
(3)判断字符长度是否相等
(4)遍历判断每个字符是否相等
四、==和equals的区别
如果是基本类型,只能用【==】,它们没有equals方法。
如果是引用类型,直接【==】的话是比较内存地址。如果这个引用类型重写过equals()方法,可以用equals()方法比较内容,如Integer、String……等常用的引用类型都是有重写过euqals()的。
五、int、Integer和new Integer()多种赋值比较区别
1.int
int无需多说,基本类型,无论是声明变量还是==比较我们都很清楚了,都只是比较值而已。
2.Integer
Integer的初始化就不太一样了
Integer i = 10;
Integer j = 10;
System.out.println(i==j);
如果这么进行赋值,那么返回是true,按照之前的说法比较的是地址那么必然是不一样的才对啊,这里就涉及了java自动装箱
系统会自动将赋给Integer变量的int值封装成一个Integer对象,例如Integer a1=3
,实际上的操作是
Integer a = Integer.valueof(3)
但是这个自动装箱也是有范围限制的,当int的范围在-128——127之间时,会通过一个IntegerCache缓存来创建Integer对象;当int不在该范围时,就直接是new Integer()创建对象
3.new Integer()
这种赋值就不用多说了,等号会比较引用,所以不会相等
4.其他包装类
例如Long和Integer是一样的,有一个-128~127的缓存,Double和Float则是没有缓存直接返回new。
六、String直接赋值和new String()比较的区别
String也需要特别的说明一下,因为它并不属于基本类型,所以没有int、long那种类型,这种情况我们只需要比较两种情况,直接赋值和new,也就是比较:
//第一组
String a=new String("test");
String b="test";
System.out.println(a==b);
//第二组
System.out.println("a"=="a");
//第三组
String a=new String("test");
String b=new String("test");
System.out.println(a ==b);
答案是 false,true,false,因为只要有一边是new的方式初始化的变量,那地址肯定是不一样的,并且这里也是用【==】进行比较地址,自然是false。
字符串常量池
关于String的直接赋值,则需要先说明一下字符串常量池。
String类是我们平时项目中使用的很多的对象类型,jvm为了提升性能和减少内存开销,以及避免字符的重复创建,专门维护了一块字符串常量池的内存空间。当我们需要使用字符串时,就会先去这个常量池中找,如果存在该字符串,就直接将该字符串的地址返回去直接用。如果不存在,则用new进行初始化并将这个字符串投入常量池中以供后面使用。
这个字符串常量池就是通过直接赋值时使用的,所以如果是直接赋值的方式初始化相同内容的String,那么其实都是从同一个常量池里取到字符串,地址指向的是同一个对象,自然结果都是相同的。
字符串的拼接比较
String a = "hello";
String d = "helloworld";
final String c = "hello";
System.out.println(d == a + "world"); //false
System.out.println(d == "hello" + "world"); //true
System.out.println(d == c + "world"); //false
如果只看内容,d都是和helloworld进行了比较,但是带有变量的就是false,纯字符串的就是true,这是为什么呢?
其实这跟jvm的编译期有关,在进行编译时,它可以识别出"hello" + “world"这样的字面量和"helloworld"是相同的,认为这是直接的字面量赋值。通过反编译其实可以看出来,编译后,它直接将"hello” + “world"编译成了"helloworld”。所以自然都是在同一个常量池里找,比较起来也是相同的。
但是加上final就有不一样了,这里由于c被视为了常量,所以同样认为是字面量赋值,最终还是在常量池中获取的值,结果就是true了。
如果有任一边是通过new赋值的,那么结果肯定是false。
如果两边都是直接赋值的,或是通过final变量进行拼接赋值的,结果是true。只要有一边有涉及非final变量,结果就是false。
七、 hashCode()
1.简介:
hashCode()方法的作用是获取哈希码,也称为散列码,它实际上只是返回一个int整数。
但是它主要是应用在散列表中,如:HashMap,Hashtable,HashSet,在其他情况下一般没啥用,原因后面会说明。
Java中的hashCode并没有真正的实现为每个对象生成一个唯一的hashCode,还是会有一定的重复几率。
object中的hashcode()
public native int hashCode();
2.重写hashCode()
核心是保证相同的对象能返回相同的hash值,尽量做到不同的对象返回不同的hash值。主要能保证核心的规则即可。例如Integer的hashcode就很简单粗暴,直接返回它所代表的的value值。也就是1的hashcode还是1,100的hashcode还是100。
public int hashCode() {
return Integer.hashCode(value);
}
String 的hashCode就相对复杂一些了通过这个公式计算的hashcode
|
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
在java的很多类中都会重写equals和hashCode方法,这是为什么呢?比如我定义两个字符相同的字符串,那么对它们进行比较时,我想要的结果应该是相等的,如果你不重写equals和hashCode方法,他们肯定是不会相等的,因为两个对象的内存地址不一样。
3.hashCode()和equals() 的关系
和equal()方法作用类似,hashCode()在java中的也是用于比较两个对象是否相等。我们应该都大概听过一句话:重写equals()方法的话一定要重写hashCode()。这里就说明一下这点。
分两种情况:
(1)首先一种情况是确定了不会创建散列表的类,我们不会创建这个类的HashSet、HashMap集合之类的。这种情况下,这个类的hashCode()和equals() 完全没有任何关系,当我们比较这个类的两个对象时,直接用的就是equals(),完全用不上hashCode()。自然,也没啥必要重写。
(2)另一种情况自然就是可能会需要使用到散列表的类了,这里hashCode()和equals()就比较有关系了:
在散列表的使用中,经常需要大量且快速的对比,例如你每次插入新的键值对,它都必须和前面所有的键值对进行比较,确保你没有插入重复的键。这样如果每个都用equals,可想而知,性能是十分可怕的。如果你的equals再复杂一些,那就凉凉了。
这时候就需要hashCode()了,如果利用hashCode()进行对比,只要生成一个hash值比较数字是否相同就可以了,能显著的提高效率。但是虽然如此,原本的equals()方法还是需要的,hashCode()虽然效率高,可靠性却不足,有时候不同的键值对生成的hashcode也是一样的,这就是哈希冲突。
在使用时,hashCode()将与equals结合使用。虽然hashcode可能会让不同的键产生相同的hashcode,但是它能确保相同的对象返回的hashcode一定是相同的(除非你重写没写好),我们只需要利用后面这点,一样可以提高效率。
在散列表中进行对比时,先比较hashCode(),如果它不相等,那说明两个对象肯定不可能相同,就可以直接进行下一个比较了。但如果hashCode()相同,因为哈希冲突的缘故我们无法直接判断两个对象是相同的,就必须继续比较equals()来获取可靠的结果。
所以如果这个类可能会创建散列表的情况下,重写了equals方法,就必须重写配套的hashcode方法,他们两个在散列表中是搭配使用的。