开发-技能-String共享机制
java中String是一个不可变类,java语言设计者将String设计成一个不可变类,其中一个重要目的是想要让字符串可以共享(String为什么设计为不可变类原因可参考“开发-习惯-知其然与知其所以然”),让包含的值相同的String能使用相同的地址空间,从而达到节省空间和提高运行速度的作用。在jvm的内存结构中,有一块专门存放字符串常量的区域叫字符串常量池。
这里有两个前提,第一就是由编译器在类编译阶段就确定了哪些字符串对象要放到字符串常量池;第二就是JVM在运行期会创建这些常量对象,用于共享(字符串常量池是静态信息,在类加载时就创建了)。一般来说,放入字符串池的String在编译时就已经确定了,但String的intern()方法可以将字符串对象增加到字符串常量池中是个例外。String的intern()方法是一个本地方法,定义为public native String intern();,当调用 intern 方法时,如果常量表已经包含一个字面值等于此 String 对象字面值的字符串常量(该对象由 equals(Object) 方法确定),则返回字符串常量池中的字符串对象;否则,将此 String 对象添加到字符串常量池中,并且返回此常量的引用。对String的intern方法的官方原文说明:When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned。
那么哪些String在编译时会加入到字符串常量池中呢?这个问题可能需要做编译器的人才可以回答,但我们可通过试验猜测一下,罗列一些个人认为在编译时可以加入到字符串常量池或不会加入到字符串常量池的情况,希望能起到一点抛砖引玉的作用。
1、String str1 = "abc";
String str2 = "abc";
if(str1 == str2){
System.out.println("StringPool3:str1 == str2");
}else{
System.out.println("StringPool3:str1 != str2");
}//打印结果是StringPool3:str1 == str2
2、String str1 = "abc"+"def";
String str2 = "abcdef";
if(str1 == str2){
System.out.println("StringPool4:str1 == str2");
}else{
System.out.println("StringPool4:str1 != str2");
}//打印结果是StringPool4:str1 == str2
3、final String str1 = "abc";
final String str2 = "def";
String str3 = str1 + str2;//final的变量内联,相当于String str3 = "abc" + "def";
String str4 = "abcdef";
if(str3 == str4){
System.out.println("StringPool2:str3 == str4");
}else{
System.out.println("StringPool2:str3 != str4");
}//打印结果是StringPool2:str3 == str4
4、String str1 = "abc";
String str2 = str1+"def";
String str3 = "abcdef";
if(str2 == str3){
System.out.println("StringPool4:str2 == str3");
}else{
System.out.println("StringPool4:str2 != str3");
}//打印结果是StringPool4:str2 != str3
5、static final String st1;
static final String st2;
static{
st1="abc";
st2="def";
}
public void StringPool6(){
String str1 = st1 + st2;
String str2 = "abcdef";
if(str1 == str2){
System.out.println("StringPool5:str1 == str2");
}else{
System.out.println("StringPool5:str1 != str2");
}
}//打印结果是StringPool5:str1 != str2
6、String str1 = new String("abc");
String str2 = new String("abc");
if(str1 == str2){
System.out.println("StringPool6:str1 == str2");
}else{
System.out.println("StringPool6:str1 != str2");
}//打印结果是StringPool6:str1 != str2
7、String str1 = "abc";
String str2 = new String("abc");
if(str1 == str2){
System.out.println("StringPool7:str1 == str2");
}else{
System.out.println("StringPool7:str1 != str2");
}//打印结果是StringPool7:str1 != str2
8、String str1 = "abc";
String str2 = new String("abc").intern();
if(str1 == str2){
System.out.println("StringPool8:str1 == str2");
}else{
System.out.println("StringPool8:str1 != str2");
}//打印结果是StringPool8:str1 == str2
考察以上这些例子,我们可以得出一些结论:
1、对直接赋值创建的String对象,比如String str1 = "abc";,编译器会将它加入到字符串常量池中。
2、对直接通过多个常量表达式赋值创建的String对象,比如String str1 = "abc"+"def";,编译器会有优化,等价于String str1 = "abcdef";,并将它加入到字符串常量池中。
3、对final的字符串常量,而且声明时就初始化好,比如final String str2 = "def";,编译器会将它加入到字符串常量池中;就算是final String str1 = "abc";final String str2 = "def";String str3 = str1 + str2;的代码,因为编译时会对final变量进行内联,所以也就相当于String str3 = "abc" + "def";,所以也会将"abcdef"加入到字符串常量池中。
4、对final的字符串常量,声明时没有初始化,需要在其他地方初始化的,比如static final String st1;,编译器就会认为st1的值在编译期是无法确定的,必须要在运行期才能确定,这样的话编译器不会将它加入到字符串常量池中。
5、对赋值语句中有字符串变量,而且这个字符串变量又不是初始化好的final常量的话,比如String str1 = "abc";String str2 = str1+"def";,则编译器也会认为str2的值在编译期是无法确定的,必须要到运行期才能确定(这在人看来str2明明也是个常量啊,估计编译器做的是事情不会那么多,只判断一层逻辑,不会追溯判断;对str2,编译后会通过StringBuffer.toString()方法来生成字符串对象,一般有两个或以上字符串拼接成的字符串,编译器一般都会使用StringBuffer来实现),这样的话编译器也不会将它加入到字符串常量池中。
6、new出来的字符串必定会新创建一个对象(不同的对象地址肯定是不同的),这样只要是new出来的字符串,用==去比肯定是不相等的。在String str1 = new String("abc");中的abc,编译器也会查找字符串常量池,如果abc存在则使用,不存在则加入到字符串常量池,这个可以通过查看java类的jvm指令来获取,通过javap -c classname这个命令,我们可以分析java类的jvm指令。
考察下面的类:
public class TestClass2{
public static void main(String[] args){
String str1 = new String("abc");
String str2 = new String("abc");
if(str1 == str2){
System.out.println("StringPool7:str1 == str2");
}else{
System.out.println("StringPool7:str1 != str2");
}
}
}
编译成TestClass2.class后,通过javap -c TestClass2来查看这个类的指令如下:
Compiled from TestClass2.java
public class TestClass2 extends java.lang.Object
public TestClass2();
public static void main(java.lang.String[]);
}
Method TestClass2()
0 aload_0
1 invokespecial #8 <Method java.lang.Object()>
4 return
Method void main(java.lang.String[])
0 new #16 <Class java.lang.String>
3 dup
4 ldc #18 <String "abc">
6 invokespecial #20 <Method java.lang.String(java.lang.String)>
9 astore_1
10 new #16 <Class java.lang.String>
13 dup
14 ldc #18 <String "abc">
16 invokespecial #20 <Method java.lang.String(java.lang.String)>
19 astore_2
20 aload_1
21 aload_2
22 if_acmpne 36
25 getstatic #23 <Field java.io.PrintStream out>
28 ldc #29 <String "StringPool7:str1 == str2">
30 invokevirtual #31 <Method void println(java.lang.String)>
33 goto 44
36 getstatic #23 <Field java.io.PrintStream out>
39 ldc #36 <String "StringPool7:str1 != str2">
41 invokevirtual #31 <Method void println(java.lang.String)>
44 return
分析上面的jvm指令,java.lang.String的Class对象是第16项常量,而abc这个字符串对象则是第18项常量,所以说在编译时String str1 = new String("abc");中abc这个字符串还是会加入到字符串常量池中的。
7、虽然是用了new String("abc")来创建字符串对象,但在调用了intern方法后,返回的仍然是字符串常量池中对象的引用,说明intern方法确实如同其方法规范所说,可将字符串对象放入常量池,并返回此常量对象的引用。
评论