话题是由如下的事情引出的:
- public class StringTest {
- public static void main(String[] args) {
- String str1 = new String("abc");
- String str2 = "abc";
- if (str1 == str2) {
- System.out.println("str1 == str2");
- } else {
- System.out.println("str1 != str2");
- }
- String str3 = "abc";
- if (str2 == str3) {
- System.out.println("str2 == str3");
- } else {
- System.out.println("str2 != str3");
- }
- str1 = str1.intern();
- if (str1 == str2) {
- System.out.println("str1 == str2");
- } else {
- System.out.println("str1 != str2");
- }
- String str4 = new String("abc");
- str4 = str4.intern();
- if (str1 == str4) {
- System.out.println("str1 == str4");
- } else {
- System.out.println("str1 != str4");
- }
- }
- }
这段程序的输出是什么?
答案:
- str1 != str2
- str2 == str3
- str1 == str2
- str1 == str4
先看看String类型的对象的产生方法:
String有一个所谓的String constant pool ,是一个特殊的一个空间(注意这个是在PermGen上的,它是JVM用来保存类定义和常量池的保留空间,缺省是64M)保存String常量。String str = “abc”是先定义一个名为str的对String类的对象引用变量:String str;再用equals方法(String类覆盖了equals方法)判断这个特殊空间(String constant pool )是否有abc,有则将原来在栈中指向abc的引用赋值给str,否则就在这个特殊空间(String constant pool )上开辟一个存放字面值为"abc"的地址,接着在堆上创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。最后将str指向对象o的地址。
str(栈)->o(堆)->abc(栈--常量池)
String str2 = new String("abc"),这里"abc"本身就是pool中的一个对象,而在运行时执行new String()时,将pool中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s持有。这条语句就创建了2个String对象。它引用堆上创建的abc对象。所以str和str2是指向不同的对象,它们是不同的。那么这样话程序的3--15行就好理解了。
而String有个intern()方法,这个方法是个本地方法,调用它就会引发一个查找的过程,要有abc存在 (这里的确有abc) ,那么它相当于告诉JVM,已经有一个内容为abc对象存在了,返回给我一个指向它的引用即可。所以16--27打印的结果是相等的。
intern这个方法使得建立String更加节省空间并且使用==判断更加快速。注意,在Java 中常常自动intern,我们看下边的这个例子。
package org.bupt.test;
public class StringInternExample {
private static char[] chars =
{'A', 'S', 't', 'r', 'i', 'n', 'g'};
public static void main(String[] args) {
// (0) 一般的,我们使用这样的语句定义一个字符串
String aString = "AString";
// (1) 对于第一个例子,我们连接两个String,他们在编译时都是已知的。
String aConcatentatedString = "A" + "String";
printResults("(1)",
"aString", aString,
"aConcatentatedString", aConcatentatedString);//true , true
// (2) 对于第二个例子,建立相同的String,但是它的内容直到运行时才知道
String aRuntimeString = new String(chars);
printResults("(2)",
"aString", aString,
"aRuntimeString", aRuntimeString);//false , true
// (3) 对于第三个例子,创建String,并且调用intern
String anInternedString = aRuntimeString.intern();
printResults("(3)",
"aString", aString,
"anInternedString", anInternedString);//true , true
// (4) 对于第四个例子,我们显式给予其值。
String anExplicitString = new String("AString");
printResults("(4)",
"aString", aString,
"anExplicitString", anExplicitString);//false, true
// (5) 对于第五个例子,我们看看传入参数对这个的测试
if (args.length > 0) {
String firstArg = args[0];
printResults("(5)",
"aString", aString,
"firstArg", firstArg);//false , false
// (6) 对于传入参数的intern测试
String firstArgInterned = firstArg.intern();
printResults("(6)",
"aString", aString,
"firstArgInterned", firstArgInterned);//false ,false
}
}
/**
* 打印 equals(...) 和 ==
*/
private static void printResults(String tag,String s1Name, String s1, String s2Name, String s2) {
System.out.println(tag);
System.out.println(" " +s1Name + " == " + s2Name + " : " + (s1 == s2));
System.out.println(" " +s1Name + ".equals(" + s2Name + ") : " + s1.equals(s2));
System.out.println();
}
}
结果如下:
(1)
aString == aConcatentatedString : true
aString.equals(aConcatentatedString) : true
(2)
aString == aRuntimeString : false
aString.equals(aRuntimeString) : true
(3)
aString == anInternedString : true
aString.equals(anInternedString) : true
(4)
aString == anExplicitString : false
aString.equals(anExplicitString) : true
(5)
aString == firstArg : false
aString.equals(firstArg) : true
(6)
aString == firstArgInterned : true
aString.equals(firstArgInterned) : true
我们可以一个个分析:
情况(1):使用了“+”这个连接字连接两个在编译时就已知的字符串,得到的结果是==和equals都为true,说明+动作在编译时就会执行。
情况(2):使用了new String(chars)创建了一个只要运行时才知道的字符串,得到的结果是==为false,equals为true,说明运行时才创建这个字符串。
情况(3):使用了intern方法创建了一个字符串,得到的结果是==和equals都为true,前边对intern分析可知intern只是返回了一个相同的引用而已。
情况(4):使用了intern方法创建了一个字符串,得到的结果是==为false,equals为true,说明new创建了一个新的对象。
情况(5):与情况(2)实质上相同。
情况(6):与情况(3)实质上相同。
好,这样一来我们就基本清楚了这个==与equals的问题了。
最后提一句:当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,因为==是用来判断两个基本数据类型是不是相同。
借用一个网友的Blog,简单的总结一下:
Java Virtual Machine maintains an internal list of references for interned Strings ( pool of unique Strings) to avoid duplicate String objects in heap memory. Whenever the JVM loads String literal from class file and executes, it checks whether that String exists in the internal list or not. If it already exists in the list, then it does not create a new String and it uses reference to the existing String Object. JVM does this type of checking internally for String literal but not for String object which it creates through ‘new’ keyword. You can explicitly force JVM to do this type of checking for String objects which are created through ‘new’ keyword using String.intern() method. This forces JVM to check the internal list and use the existing String object if it is already present.
So the conclusion is, JVM maintains unique String objects for String literals internally. Programmers need not bother about String literals but they should bother about String objects that are created using ‘new’ keyword and they should use intern() method to avoid duplicate String objects in heap memory which in turn improves java performance. see the following section for more information.
其中有些细节问题暂时无从求证,请带有批判眼光看这篇小文。
我们最后判断一下这些的问题:
1."a" == "a"
2."a"+"b" == "ab"
3. "a".toLowerCase()=="a"
4. "a"+"b".toLowerCase() == "ab"
5."A".toLowerCase() == "a"
以上这些语句用sysout输出,哪些是true,哪些是false?
1.true,原因如上。
2.true,两个确定的字符串常量,经编译器优化后在class中就已经是"ab“,在编译期其字符串常量的值就确定下来了。
3.true,如果没变的话,返回的就是原来的字符串的对象引用。
4.false,对字符串进行小写化操作后得到一个对象引用,再对字符串相加,存在引用的+操作在编译期无法确定结果,即"a" +"b".toLowerCase() 无法被编译器优化,在程序运行期,动态分配并将对象引用返回。 所以"ab"在字符串常量池里,"a" +"b".toLowerCase() 在堆上,"=="操作符比较的是内存地址,因此比较的结果为false.
5.false,新建一个字符串对象引用返回。
那么就顺带说一说堆和栈。
堆是动态地分配内存大小,生存期也不必事先告诉编译器,垃圾自动回收负责回收,但由于在运行时动态分配内存,存取速度较慢。在堆上分配空间是通过new等指令建立的,类实例化的对象就是从堆上去分配空间的。
栈中主要存放一些基本类型的变量(int、short、 long、byte、float、double、boolean、char)和对象句柄,存取速度比堆要快。注意包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些不是在栈上的。
另外,栈数据可以共享。
例如:
int a = 5;
int b = 5;
它的工作方式是这样的。
JVM处理int a = 5,首先在栈上创建一个变量为a的引用,然后去查找栈上是否还有5这个值。如果没有找到,那么就将5存放进来,然后将a指向5。接着处理int b = 5,在创建完b的引用后,因为在栈中已有5这个值,便将b直接指向5。
于是,就出现了a与b同时指向5的内存地址的情况。
下午和一个线上的朋友聊起这些事情,他给了建议去看JVM的相关书籍,还介绍了用javap来看,方法都很好的。