对于字符串来说,对象的引用存储于栈中,编译期已确定的(直接用双引号定义的)存储在常量池中,任何用new创建的字符串对象(运行期产生)存储于堆中。
String s1="Hello"; //对象的引用s1存储在栈中,"Hello"存储在常量池中;
String s2=new String("world");//对象的引用s2存储在栈中,"world"首先会在常量池中创建,当new执行时,在堆中创建新的对象,将常量池中的"world"复制到堆中。
new我们比较好理解,每当用new创建对象时,不管这些对象的内容是不是一样的,都会在堆中就会开辟一个新的内存空间。也就是说这些对象的空间是不共享的(关于共享的概念在下面常量池中会解释的更加清楚)。下面来讲解常量池的存储原理。
常量池的来源
JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存就是常量池。
java中的常量池技术
为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个,有就直接取出来,不会重复创建),则在需要重复创建相等变量时节省了很多时间。
可以想象将各种字符串存放在公共的存储池中,字符串变量指向存储池中相应的位置,如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。
工作原理
当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象,则返回这个对象的引用,否则新的字符串对象被创建,然后返回这个新对象的引用。
首先说明一点,在java中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。
看下面的例子:
public class StringDemo1 { public static void main(String[] args){ String s1="Hello"; String s2="Hello"; //"Hello"字符串对象已存在,不会重新创建新的字符串对象 System.out.println(s1==s2); //true String s3=new String("Hello"); String s4=new String("Hello") System.out.println(s3==s4); //false System.out.println(s1==s3); //false } }
很明显,当我们使用了new来构造字符串对象的时候,不管字符串常量池中有没有相同内容的对象的引用,新的字符串对象都会创建。
关于java的连接表达式"+",看这个例子:
public class StringDemo2 { public static void main(String[] args){ String s1="Hello"; String s2="World"; String s3="HelloWorld"; String s4=s1+s2; System.out.println(s3==s4); //false String s5="Hello"+"World"; System.out.println(s3==s5); //true } }
s5虽然是动态拼接出来的字符串,但是所有参与拼接的部分s1,s2都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接拼好.因此String s5="Hello"+"World";在class文件中被优化成String s5 = "HelloWorld";
(只有字面量的拼接才会加入到常量池中,我是这么理解的)。因此s3==s5的结果并不出乎意料。
那如何理解s3==s4返回false呢?
String s4=s1+s2实质相当于String s4=(new StringBuilder()).append(s1).append(s2).toString()
对于所有包含new方式新建对象(包括null)的"+"连接表达式,它所产生的新对象都不会被加入字符串池中。当然这只是比较浅显的理解,
虽然s1、s2在赋值的时候使用的字符串字面量,但是拼接成s4的时候,s1、s2作为两个变量,都是不可预料的,所以不做优化。等到运行时,s1和s2拼接的字符串存放在堆中地址不确定,不可能与常量池中的"HelloWorld"地址相同。
因此下面的程序应该会更加好理解:
public class StringDemo3 { public static final String s1="Hello"; public static final String s2="World"; public static void main(String[] args){ String s3="HelloWorld"; String s4=s1+s2; System.out.println(s3==s4); //true } }
s1,s2定义为常量已赋值在编译时就存放进常量池中了,因此s4也固定了。如果
关于java程序编译和运行的过程不太明白,参考文章:java程序编译和运行过程
这么看来,好像返回的结果取决于s1与s2是常量还是变量,以及它们何时被赋值。
之前还看到一个更有意思的程序:
public class StringDemo4 { public static final String s1; public static final String s2; static{ s1="Hello"; s2="World"; } public static void main(String[] args){ String s3="HelloWorld"; String s4=s1+s2; System.out.println(s3==s4); } }
关于执行结果有兴趣的同胞可以自己尝试一下。
实际上java的6种基本类型的包装类都实现了常量池技术(Byte,Short,Integer,Long,Character,Boolean),其他两种浮点数类型的包装类并没有实现常量池技术(Float,Double)。这个可以通过程序验证下。