1.String的不可变性
String str = new String("abc");
在内存中开辟了一块空间之后,该空间赋值"abc",该空间中的值即为"abc",无法改变,除非经过GC后,内存重新分配
从源码中分析,String底层是被final修饰的char数组,从jdk9之后,是被final修饰的byte数组,是不可变量
(从char改为byte大概原因是大部分String数据是字母或者拉丁文,只占一个byte,而一个char是两个byte,浪费资源空间而一个汉字占用两个byte,可以通过设置编码UTF8指定)
例1:
String s1 ="abc";
String s2 ="abc";
log.info(s1 == s2);//true
s1 ="def";
log.info(s1 == s2);//false
log.info(s1);//def
log.info(s2);//abc/**
* String s1 = "abc";
* 以字面量的方式赋值,"abc"存储在堆空间中的字符串常量池中, * 而字符串常量池中不允许相同的字符串出现,此"abc"的地址为0x001
* String s2 = "abc";
* 常量池中已经存在"abc",所以将0x001给变量s2,此时s1和s2指向同一个地址
* s1 = "def";
* "abc"并未被修改为"def",而是在字符串常量池中开辟新的空间赋值"def",地址为0x002,
* 然后将此地址给变量s1,此时s1指向的是地址0x002,而s2指向的地址是0x001
*/
例2:
/**
* String str = "aaa";
在内存中开辟空间,赋值"aaa",地址为0x001
public void change(String str) {
str = "bbb";
}
然后调用changge()方法,changge()入栈
str = "bbb";
在空间中开辟新的空间,赋值为"bbb",地址为0x002,将地址给局部变量str,方法出栈
main()方法中的实例变量str指向的是0x001地址,change()方法中的局部变量str指向的是0x002的地址
log.info(demo.str);打印的是实例变量str指向的地址0x001,aaa
*/
2.String的内存分配
jdk6以及以前,String对象存储在永久代中
jdk7以及以后,String对象存储在java堆中
3.字符串的拼接操作
1.常量与常量的拼接结果在常量池,原理是编译器优化
2.常量池中不会存在相同内容的常量
3.只要其中还有一个是变量,结果就在堆中(而不是在常量池中),变量拼接的原理是StringBuilder
4.如果拼接的对象调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
例1:
String s1 = "a"+"b"+"c"; //编译器优化后等同于abc,地址为0x001
String s2 = "abc"; //字面量赋值,abc是在常量池中,将此地址0x001给了s2
log.info("{}",s1==s2); //true
log.info("{}",s1.equals(s2)); //true
查看字节码文件
例2:
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop"; //编译器优化
//如果拼接符号前后有了变量,则相当于在堆中new String(),具体的内容为拼接的结果:javaEEhadoop
//所以,s5,s6,s7是在堆空间中是三个不同的对象
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
log.info(s3 == s4); //true
log.info( s3 == s5); //false
log.info( s3 == s6); //false
log.info(s3 == s7); //false
log.info( s5 == s6); //false
log.info(s5 == s7); //false
log.info(s6 == s7); //false
//intern():判断字符串常量池中是否存在javaEEhadoop,如果存在,则返回常量池中javaEEhadoop的地址;
//如果不存在,就在字符串常量池中加载一份javaEEhadoop,并返回这个对象的地址
String s8 = s6.intern();
log.info("{}", s4 == s8); //true
例3:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
log.info("{}",s3 == s4); //false
对该代码的字节码文件进行反编译后
ldc #16 //这个a就是字符串a
astore_1 //将这个字符串a存储在局部变量表的1位置
ldc #17 //这个b就是字符串b
astore_2 //将这个字符串b存储在局部变量表的2位置
ldc #18
astore_3 //将这个字符串ab存储在局部变量表的3位置
new #11
dup
invokespecial #12
aload_1
invokevirtual #13
aload_2
invokevirtual #13
invokevirtual #14
astore 4
//在执行s1 + s2的时候,是在堆空间中new了一个StringBuilder,然后从局部变量表的1位置获取字符串a,
执行append()方法将字符串a追加到StringBuilder中,然后再从局部变量表的2位置获取字符串b,调用append()方法
将字符串b追加到StringBuilder中,最后调用toString()方法(约等于new String()),所以s1+s2的值是在堆内存中
重新开辟了一块空间的值,而s3是在堆中的字符串常量池中,并不是同一个对象
例4:
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
log.info("{}",s3 == s4); //true
对该代码的字节码进行反编译后
astore_1
ldc #17
astore_2
ldc #18
astore_3
ldc #18
astore 4
//可以看到被final修饰后,s1,s2其实就是常量的引用,而不是变量,s4 = s1+s2可以认为是s4 = "a"+"b"
结论:字符串拼接操作不一定使用的是StringBuilder
如果拼接符号左右两边都是字符串常量或者常量引用的话,则仍然使用编译器优化,即非StringBuilder的方式
针对final修饰类,方法,基本数据类型,引用数据类型的变量时,能用final修饰的尽量用final修饰
4. String str = new String("abc")创建了几个对象?
两个,看字节码
1.new关键字在堆中分配空间
2.ldc指令从字符串常量池中加载"abc",如果没有就在常量池中创建一个
String str = new String("a")+new String("b")
然后查看StringBuilder.class的字节码文件,找到toString()方法
toString方法中,第一行在堆中new了一块空间,并没有在字符串常量池中创建"ab"对象
所以,上面的操作一共创建了六个对象
1.StringBuilder对象
2.String对象,值时"a"
3.字符串常量池中的"a"
4.String对象,值时"b"
5.字符串常量池中的"b"
6.toString方法中new对象,存放"ab"字符串
5.intern()的理解
总结String的intern()的使用
jdk1.6中,将这个字符串对象尝试放入字符串常量池中
如果池中有,则不会放入,返回已有的池的对象地址
如果池中没有,会把此对象赋值一份,放入池中,并返回池中的对象地址
jdk1.7中,将这个字符串尝试放入字符串常量池中
如果池中有,则不会放入,返回已有的池的对象地址
如果池中没有,会把对象的引用地址赋值一份,放入池中,并返回池中的引用地址
例1:
String s = new String("1");
s.intern();
String s2 = "1";
log.info("{}",s == s2); //false
/**
* new String("1");在堆中new一个对象存放字符串1,在字符串常量池中开辟一块空间存放字符串1,此时s指向的是堆空间中的字符串1
* s.intern();执行intern()方法,但是此时字符串常量池中已经存在字符串1,所以s.intern()方法没有任何作用,s指向的还是堆空间中的字符串1
* String s2 = "1"; 通过字面量赋值,s2指向的是字符串常量池中的1
* 所以s1,s2是指向不同的地址,结果为false
*
*/
例2:
String s = new String("1") + new String("1");
s.intern();
String s2 = "11";
log.info("{}",s==s2); //jdk6 false;jdk7/8 true
/**
* new String("1") 在堆中new一个对象存放字符串1,在字符串常量池中开辟一块空间存放字符串1
* new String("1") 在堆中new一个对象存放字符串1,由于字符串常量池中已经存在字符串1,所以不再创建字符串1
* s.intern() jdk6:此时s的值是指向堆空间中的字符串11,去永久代的字符串常量池中查询,没有字符串11,在字符串常量池中开辟空间存入字符串11
* jdk7:此时s的值是指向堆空间中的字符串11,去堆中的字符串常量池中查询,没有字符串11,但是因为字符串常量池也在堆空间中,为了节省内存
* 字符串常量池中开辟的新空间的地址就是堆空间中的11的地址
* String s2 = "11" 字面量赋值会直接在字符串常量池中寻找,有,就将地址给s2,没有,就在字符串常量池中创建,
* 而此时,显然上一步已经在字符串常量池中有了字符串11
*/
例3:
String s = new String("1") + new String("1");
String s2 = "11";
s.intern();
log.info("{}", s == s2); //false
s = s.intern();
log.info("{}", s == s2); //true
/**
* new String("1") 在堆中new一个对象存放字符串1,在字符串常量池中开辟一块空间存放字符串1
* new String("1") 在堆中new一个对象存放字符串1,由于字符串常量池中已经存在字符串1,所以不再创建字符串1
* String s2 = "11" 这个操作会在字符串常量池中开辟一个空间,存放字符串11
* s.intern() 此时在字符串常量池中已经存在了字符串11,所以这行代码没有什么效果
s指向的是堆中的对象,s2指向的是栈中的对象
s = s.intern(); 字符串常量池中已经存在"11",将常量池中的引用给变量s,s2指向的也是常量池中的"11"
*/