预备知识
equals、==
首先明确一点,==比较的是引用,equals比较的是内容,在类库没有定义equals方法重写的情况下,自然继承的是Object类的equals方法,上源码:
public boolean equals(Object obj) {
return (this == obj);
}
那么对于String类来说,官方已封装了复写之后的equals方法,比较的是具体内容,判断:如果是引用相同,也就是堆内的地址相同,那么就是同一个对象,直接返回true,否则就遍历字符串中的每一个字符进行比较:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
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;
}
其次,字符串还有个常量池,也就是说如果先定义了一个"ABC"字符串,再定义一个相同字符串的时候,会首先去常量池里面找之前有没有定义,如果有,则直接指向常量池的同一地址。
常量池介绍
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
-
为字符串开辟一个字符串常量池,类似于缓存区
-
创建字符串常量时,首先坚持字符串常量池是否存在该字符串
-
存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
有了以上的铺垫,后面几个问题就好解释了:
案例1
public class StringTests {
public static void main(String[] args) {
String str1 = "ABC";
String str2 = "ABC";
String str3 = new String("ABC");
System.out.println(str1 == str2);// true
System.out.println(str2 == str3);// flase
System.out.println(str1.equals(str2)); // true
System.out.println(str2.equals(str3)); // true
}
}
str1与str2都指向的是常量池中同一个字符串的地址,所以==为true,而str3是new了一个新的对象,会在堆中新建一个内存地址,所以比较str3和str3的地址时则为flase,equals就简单了,比较的均为字符串内容,故均为true。
案例2
public class StringTests {
public static void main(String[] args) {
String str1 = "ABC";
String str2 = "ABC";
String str3 = new String("ABC");
String str3_1=str3.intern();
System.out.println(str1 == str2);// true
System.out.println(str2 == str3);// flase
System.out.println(str3_1 == str1);// true
System.out.println(str3_1 == str3);// flase
}
}
如果我们新定义一个str3_1变量,让他取str3的intern()方法,此方法是这样定义的:
这是一个native的方法,书上是这样描述它的作用的:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回常量池池中这个字符串的String对象;否则,将此String对象包含的字符添加到常量池中,并返回此String对象的引用。
所以str3_1拿到的是常量池中的”ABC“地址,和str1、str2的地址肯定是相同的,
案例3
public class StringTests {
public static void main(String[] args) {
String str1 = "ABC";
String str2 = "ABC";
str1 +="D";
String str5 = "ABCD";// 肯定在常量池中生成一个
System.out.println(str5 == str1);//flase
}
}
这里如果给str1拼接了一个D字符串,这里通过javap命令查看字节码文件,会发现+=操作在String源码中调用的其实是apped方法,生成的是一个新的对象堆地址,str5指向的常量池的地址与str1指向的堆对象地址不一,如果我在str1拼接之后加上str1 = str1.intern(); 这时候由于str1指向的是常量池中的ABCD,==就为true了。
public class StringTests {
public static void main(String[] args) {
String str1 = "ABC";
String str2 = "ABC";
str1 +="D";
str1 = str1.intern();
String str5 = "ABCD";// 肯定在常量池中生成一个
System.out.println(str5 == str1);//true
}
}
这里intern调用之后,如果常量池中没有”ABCD“,则会在常量池生成,如果有则直接指向它。所以此处的str1指向的是常量池中的”ABCD“,后面定义的str5在构造时则会直接指向常量池中的”ABCD“
如果这里把str1 += ”D“修改一下成常量与常量的拼接:
public class StringTests {
public static void main(String[] args) {
str1 ="ABC" +"D";
String str5 = "ABCD";// 肯定在常量池中生成一个
System.out.println(str5 == str1);//true
}
}
如果两个常量进行拼接,编译器会自动优化成"ABCD",也就是存到了常量池中去了,str5指向的也是常量池中的值。
总结
原则:
(1)常量+常量:结果是常量池
(2)常量与变量 或 变量与变量:结果是堆
(3)拼接后调用intern方法:结果在常量池
new 出来的是在堆空间