一开始这个问题并没有想专门写一篇文章,但最近看到boss上的一个提问,让我有专门针对这个知识点写一篇文章的打算。
下面会给出各种代码,随之会给出创建了几个对象的答案,然后便是相应的解释。
问题一:
String s = "a";
创建0个或1个对象。
若常量池中已经存在"a",那么就不会再创建对象,直接将引用赋值给s,这样就创建了0个对象;
若常量池中没有"a",就会在常量池中创建一个对象"a",并将引用赋值给s,这样就创建了1个对象。
问题二:
String s = new String("a");
创建1个或2个对象。
若常量池中已经存在"a",那么就不会再在常量池中创建对象,而是执行new操作,在堆中创建一个存储"a"的String对象,对象的引用赋值给s,这样就创建了1个对象;
若常量池中没有"a",就会在常量池中创建一个对象"a",然后再执行new操作,在堆中创建一个存储"a"的String对象,对象的引用赋值给s,这样就创建了2个对象。
注意:
这里的情况其实堆中的String对象的char value[]属性指向常量池中的char value[],可以通过debug的形式查看两个String对象的value值的引用相同,也就是说,虽然是两个对象,但它们的value值均指向常量池中的同一个地址。
上述过程中检查常量池是否有相同Unicode的字符串常量时,使用的方法便是String中的intern()方法。
public native String intern();
问题三:
new String("abc");
创建1个或2个对象。
同上,只是没有赋值给引用的操作。
下面的问题暂且不考虑常量池中是否已经存在对应字符串的问题,假设都不存在对应的字符串。
问题四:
String s = "a" + "b";
这里涉及到字符串常量重载“+”的问题,当一个字符串由多个字符串常量拼接成一个字符串时,它自己也肯定是字符串常量。字符串常量的“+”号连接Java虚拟机会在程序编译期将其优化为连接后的值。
在编译时已经被合并成“ab”字符串,因此,只会创建1个对象。并没有创建临时字符串对象a和b,这样减轻了垃圾收集器的压力。
问题五:
String s = "a" + new String("b");
创建5个对象。
上述的代码Java虚拟机在编译的时候同样会优化,会创建一个StringBuilder来进行字符串的拼接,实际效果类似:
String s = new String("b");
new StringBuilder().append("a").append(s).toString();
很显然,常量池中分别有“a”和“b”,堆中对象new String(“b”)和“ab”,还多出了一个StringBuilder对象,那就应该是5个对象。
疑问:
为什么会有人说,StringBuilder最后toString()之后的“ab”难道不在常量池存一份吗?这样不就是6个对象?
解析:
答案是不对的。
StringBuilder的toString()方法是如何将拼接的结果转化为字符串的:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
很显然,在toString方法中又新创建了一个String对象,而该String对象传递数组的构造方法来创建的:
public String(char value[], int offset, int count)
也就是说,String对象的value值直接指向了一个已经存在的数组,而并没有指向常量池中的字符串。
所以,上面答案是创建了5个对象。
附录:
String s1 = "a";
String s2 = new String("b");
String s3 = s1 + s2;
String s4 = "ab";
System.out.println(s3 == s4);//false
为什么会是false呢?
s4很明显存在于常量池中,那么既然s3==s4是false,那么说明s3和s4的物理地址是不一样的,既然s3不在常量池中,那么就是在堆中了。这也是为什么上面会说“ab”是堆对象。