看了好多篇博客,大家对字符串常量池的理解各有所见,自己对此也进行了探究,并将自己的理解加验证进行了整理,希望能和大佬们共同讨论。
一、字符串常量池的位置
这个比较确定,Jdk1.6及以前是存在与方法区(永久代)中,而Jdk1.7之后(本人现在用的1.8.0_192)字符串常量池被移到了堆内存中。
二、字符串的存储位置
字符串创建过程不同对应的字符串的存储位置也不同,主要有两种没有异议:
1.直接将字符串常量赋给字符串变量
String str="abcd";
对于这种语句,将在字符串常量池中创建一个"abcd"字符串对象,并将此对象的应用返回个str,并且如果字符串常量池中存在此字符串对象(用equals(oject)方法确定),直接返回该对象的应用。
2.使用new语句来创建字符串
String str=new String("abcd");
对于这种语句,会先判断"abcd"是否已经存在于字符串常量中(用equals(oject)方法确定),若存在,则在堆内存中创建一个"abcd"对象,并返回其对象的应用;若不存在,则先在字符串常量池中创建一个"abcd"字符串对象,之后再在堆内存中创建一个"abcd"对象,并返回堆内存对象的应用。
接下来来看一下String类的构造:
public final class String {
private final char value[];//String类底层对字符串的封装为不可变字符串数组
private int hash; // Default to 0
public String(String original) {//String类的构造函数之一
this.value = original.value;
this.hash = original.hash;
}
}
由上段代码可以看出,在new String("abcd")的时候会对创建在堆内的字符串对象进行初始化,且其初始化只是将字符串常量池中字符串的底层实现的应用赋值给了堆内存的字符串对象的底层实现,换句话说,字符串常量池中字符串的底层实现和堆内存中字符串的底层实现指向同一个字符串数组。其结构如下图所示:
下面在假定前边两种情况没有问题的基础上讨论其他的情况:
3.字符串的+运算
-
String str="ab"+"cd";
因为"ab"、"cd"都是常量,程序在编译时会自动把他们优化为String str="abcd";故而这个操作和1操作相同。
对编译完生成的字节码使用javap进行反编译可以看出程序在编译时做出的优化。
例如:程序如下
public class Test { String s1="a"; String s2="b"; String s3="a"+"b"; String s4=s1+s2; }
编译完反编译的结果如下:
public class test.Test { java.lang.String s1; java.lang.String s2; java.lang.String s3; java.lang.String s4; public test.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String a 7: putfield #3 // Field s1:Ljava/lang/String; 10: aload_0 11: ldc #4 // String b 13: putfield #5 // Field s2:Ljava/lang/String; 16: aload_0 17: ldc #6 // String ab 19: putfield #7 // Field s3:Ljava/lang/String; 22: aload_0 23: aload_0 24: getfield #3 // Field s1:Ljava/lang/String; 27: aload_0 28: getfield #5 // Field s2:Ljava/lang/String; 31: invokedynamic #8, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 36: putfield #9 // Field s4:Ljava/lang/String; 39: return }
由17,19行可以看出,程序直接给s3赋值为"ab"。而s4则是在程序运行时动态的赋值。
-
String str=new String("ab")+new String("cd");
结论:相当于执行两次2上的操作,因此会在常量池里创建"ab"、"cd"字符串对象(若不存在)。而在堆中会创建"ab"、"cd"对象和"abcd"对象(由于"abcd"对象是在程序执行过程中产生的,并且String类为不可变类,故而只会在堆中创建此对象)。
-
String str=new String("ab")+"cd";
结论:在堆内存创建一个ab对象,abcd对象,在常量池创建一个cd对象(若不存在),将堆内存的abcd对象的引用返回给变量str。
-
String s1="ab";String s2="cd";String s3=s1+s2;
结论:对于s3来说,只在堆内存创建abcd对象,并将对象的引用返回个s3。
-
String s1="ab";String s2=s1+"cd";
结论:对于s2来说,在堆内存创建abcd对象,并在常量池创建cd对象(若不存在),并将堆内存的abcd对象的引用返回给s2。
-
final String s="ab";String s2=s+"cd";
结论:final修饰的s可以看作常量,s2等同于String s2="ab"+"cd";
以上结论的证明均可采用String类的intern()方法。接下来介绍intern()方法。
三、intern()方法
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对向的引用。
看一个代码:
public class Test{
public static void main(String[] args){
String str1=new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern()==str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
}
}
这段代码在JDK1.6中运行,会得到两个false,而在JDK1.7中运行,会得到一个true和一个false。产生差异的原因:
- 在JDK1.6中,intern()方法会把首次遇到的字符串实例复制到常量池中,返回的也是常量池中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用。
- 而JDK1.7的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例的引用,因此intern()返回的引用和由StringBuilder创建的哪个字符串实例可以是同一个。
本文用的实验环境为JDK1.8,故而由上边程序得到的第二个false结果可推断出"java"这个字符串在str2.intern()之前已经存在于字符串常量池中了。因此由intern()的这个特性可以用来证明二中的部分结论,举例如下:
public class Test {
public static void main(String[] args) {
String s1="a";
// String s5=s1+"cd";
String s6=new String("c")+new String("d");
System.out.println(s6.intern()==s6);
}
}
如果注释掉s5行,输出结果为true,如果将s5行去掉注释,输出结果为false。因此,证明了对于String s5=s1+"cd"语句,会在常量池中创建cd对象。
参考