String Table
1、字符串的声明方式:
-
字面量的形式:直接用双引号引起来;
-
通过new的方式产生字符串对象;
public void test() { String str1 = "hello"; //字面量的形式 String str2 = new String("hello"); //new关键字的形式 }
2、String类被声明为final类,不可继承;
3、随着JDK的发展,一些底层实现的演变:
- JDK9之前底层的实现都是char数组,JDK9之后改用byte数组;
- 原因:字符串作为堆中主要的对象,使用程度是非常的高,但是由于大多数时候都是1个字节就能够存放完,因此使用char数组的话就会导致有一半的内存被浪费。
- 对于char[]与byte[],如果是拉丁字符的话就使用byte[],如果是其他的就继续使用char[],提高内存的使用率;
- StringBuffer、StringBuilder都做了相应的修改
4、字符串常量池中不会存放相同内容的字符串;
- String的StringPool是一个固定大小的HashTable,默认值长度时1009,如果放进StringPool的String太过就会导致Hash冲突严重,性能骤降;当链表过长时,当调用String.intern时性能就会大幅下降。
- 参数 -XX:StringTableSize可以设置StringTable的大小;
- 在JDK6中,StringTable大小是固定的,为1009,设置没有要求;
- 在JDk7中,StringTable默认大小为60013,设置没有要求;
- jdk8开始,最小只能设置为1009;
5、intern()的使用;
- 在JDK6中:将这个字符串尝试放进字符串常量池中:
- 如果字符串常量池中有,则并不会放入,返回字符串常量池中已有对象的地址;
- 如果没有,会把此对象复制一份(等于是创建一个新的对象,值是这个字符串),放入池中,并返回此对象的地址;
- 在JDK7及以后的版本:
- 如果池中已有,则并不会放入,返回池中已有对象的地址;
- 如果没有,则会把对象的引用地址复制一份,放入池中,并放回池中的引用地址;
6、字符串的拼接:
-
常量与常量的拼接结果在常量池,原理是编译期的优化;
public void test(){ String str1 = "a" + "b" + "c"; String str2 = "abc"; } //经过编译期的优化之后,结果就会变成以下 public void test(){ String str1 = "abc"; String str2 = "abc"; }
-
常量池中不会存在相同内容的常量;
-
只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder;
public void test2() { String str1 = "a"; String str2 = "b"; String str3 = "ab"; String str4 = str1 + str2; //此处有变量,相当于在堆中创建一个新的String对象,即进行了new String(); System.out.println(str3 == str4) //false; } /** * String str4 = str1 + str2 的底层实现细节 * ①StringBuilder sb = new StringBuilder(); * ②sb.append("a"); * ③sb.append("b"); * ④return sb.toString(); //值得一提的是:toString()的效果只是约等于new String() * 在jdk5之前用的都是StringBuffer */
-
如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址;
public void test3() { String str1 = "a"; String str2 = "b"; String str3 = "ab"; String str4 = str1 + str2; System.out.println(str3 == str4.intern()); //true //intern()方法相当于先判断字符串常量池中是否已有字符串“ab”,如果没有,就会将str4这个字符串放进字符串常量池中,然后将地址返回;如果已存在,则会直接返回该字符串地址,因此得到true的结果; }
-
字符串拼接不一定都是使用StringBuilder,如果拼接符号左右两边都是字符串常量或者常量引用,则仍然使用编译期优化
public void test4() { final String str1 = "a"; //final关键字修饰 final String str2 = "b"; String str3 = "ab"; String str4 = str1 + str2; System.out.println(str3 == str4) //true; }
7、new String(“ab”)会创建多少个对象?
- 答案是两个;
- 分析:
- 一个对象是:new关键字在堆空间中创建;
- 另一个对象:字符串常量池中的对象"ab",字节码指令:ldc
- 详细见底部 图解一;
8、拓展:new String(“a”) + new String(“b”)又会产生多少个对象?
- 答案:6个;
- 分析:
9、代码及字节码图
public class Test {
public static void main(String[] args) {
String s = new String("1"); //此处完成两个动作,一个是在堆中new一个String对象,然后也会在字符串常量池中创建一个对象,这两个对象是独立的,只是值一样而已;见图解一
s.intern(); //此行代码无实际意义,因为字符串常量池中已存在“1”
String s2 = "1";
System.out.println(s == s2); //false
String s3 = new String("1") + new String("1"); //详细可以看上面第八点以及下面图解
s3.intern();
String s4 = "11"; //这里需要补充一点:如果将该行代码提前至s3前面,结果就会是false,无关版本问题;因为s3.intern()会先复制一份s3对象的引用保存在字符串常量池中,由于s4的声明提前了,因此s3与s4就是两个不同的对象,它们只是字符串的值相同而已
System.out.println(s3 == s4); //jdk6:false; jdk7及之后: true;
}
}
图解一:
图解二:
图解三: