开门见山
- 常量与常量的拼接结果在常量池,原理就是编译器优化
- 常量池中不会存在相同内容的常量
- 只要拼接的元素中,有一个是变量,结果就是在堆中,变量拼接的原理是StringBuilder
- 如果拼接的结果调用了intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
常量拼接
String s1 = "a" + "b" + "c";
String s2 = "abc";//abc一定是放在字符串常量池中的,此地址赋给s2
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));//值比较一定为true
//实际上编译器将做出如下编译优化
String s1 = "abc";//abc作为字面量将被放入常量池
String s2 = "abc";//s2相当于使用了已有的abc
//所以System.out.println(s1 == s2); 输出为true
拼接元素中有变量
String s1 = "a";
String s2 = "b";
String s3 = "c";
//如下拼接相当于在堆空间中new String(),并非在字符串常量池,只不过字符串对象结果为abc
String s4 = s1 + "b" + "c";
String s5 = "a" + s2 + s3;
String s6 = a + b + c;
//所以 s4 == s5 false, s4 == s6 false, s5 == s6 false
intern()方法
intern()是判断字符串常量池中是否存在该值,如果存在,则返回常量池中该值的地址;如果字符串常量池中不存在该字符串值,则在常量池中加载一份该字符串常量,并返回此字符串常量的地址
String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = "abc";//此时字符串常量池已经存在abc
//如下拼接相当于在堆空间中new String(),并非在字符串常量池,只不过字符串对象结果为abc
String s5 = s1 + "b" + "c";
String s6 = "a" + s2 + s3;
String s7 = a + b + c;
String s8 = s6.intern();
//s8 == s4 true
String.intern()能保证在字符串常量池中创建唯一的字符串常量。
字节码指令角度来看字符串的拼接
s1+s2的字节码指令分析
- new StringBuilder()。在JDK5之前是使用StringBuffer,JDK5之后使用的是StringBuilder
- aload_1获取操作数栈上索引为1的操作数,也就是s1,实际上在局部变量表中的s1指向的是堆中字符串常量池中的"a"
- StringBuilder.append("a")
- 同step2一样,获取了操作数栈中的s2的值
- StringBuilder.append("b")
- StringBuilder.toString()返回结果
优化建议
- 基于该实例,所以在大量操作字符串的拼接的时候(循环中拼接字符串),不要直接使用变量进行拼接,由于每次都会创建一个StringBuilder,并且每次都会调用StringBuilder.toString()方法,toString方法实际还是创建new了一个String对象。而是声明一个StringBuilder对象去append字符串,这样的效率远高于字符串拼接的方式。
- 同理可以,大量的字符串拼接可能会造成堆空间内存的浪费,更容易会引起GC的发生。
- 在实际的开发中,如果可以基本确定循环拼接的字符串长度不高于某个限定值,建议使用构造器StringBuilder(int capacity),显式的指定StringBuilder底层的char[]数组的长度,以免过长的字符串拼接导致StringBuilder的扩容情况发生(会创建一个新的更大的char[]进行拷贝)
字符串拼接操作不一定使用的是StringBuilder
如果参与拼接的两个元素是字符串常量或者常量引用(下面final修饰之后就是常量引用了),则仍然使用编译器优化,并非创建StringBuilder
new String("ab")创建了几个对象
一个存放在堆中new的String对象,一个字符串常量池的常量字符串。
new String("a") + new String("b")创建了几个对象
- new StringBuilder(),用作字符串拼接
- new String()
- 字符串常量a
- new String()
- 字符串常量b
- StringBuilder.toString(),也会new一个String对象
JDK7之前与之后关于intern()的区别
- JDK6中,将该字符串对象放入字符串常量池中
- 如果池中有,则不会放入,返回串池中已有的对象的地址
- 如果池中没有,则会把此字符串对象赋值一份,放入池中,并返回该池中该对象的地址
- JDK7之后
- 如果池中有,则不会放入,返回串池中已有的对象的地址
- 如果没有,则会把对象的引用地址复制一份,放入字符串常量池中,并返回池中的引用地址
根据以上解释尝试理解如下代码在JDK6和JDK7之后的区别
String s3 = new String("1") + new String("1");//s3作为一个变量,指向堆空间中的String对象,值为11,然后字符串常量池中并没有11
s3.intern();//在字符串常量池中生成11
String s4 = "11";//使用的是上一行代码在字符串常量池中生成的11的地址
System.out.println(s3 == s4);