1. String基本特性:
1.1 String类是final修饰的,不可继承
1.2 String的实现原理是基于char数组,jdk9是修改为byte数组
1.3 String代表不可变的字符序列,简称:不可变性
1.4 声明一个String类型的变量大部分都是以字面量的方式,例如:String str ="abc";
关于不可变性的理解:
1.3.1 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值
1.3.2 当对现有的字符串进行拼接时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
1.3.3 当调用string的 replace方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
以下代码为例:
s1和s2都是指向堆中的字符串常量池,当对s2进行拼接时,不会直接去修改s2内存地址中的内容,而是重新在字符串常量池开辟一块内存存放s2修改之后的内容,见下图:
未对字符串s2修改之前:
修改字符串s2之后:
2. 不同jdk版本下StringTable的内存分配
jdk6
jdk7之后(该文都是按照jdk7之后的标准讲解)
StringTbale为什么要调整?
因为永久代的回收效率很低,在full GC的时候才会触发。而Full GC是老年代空间不足、永久代空间不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
3.关于字符拼接面试题解释:
3.1 常量与常量的拼接结果在常量池,原理是编译期间优化
3.2 常量池中不会存放相同内容的常量(节省空间)
3.3 只要其中有一个是变量,结果就在堆中。变量拼接的原是StringBuilder
3.4 如果拼接调用intern方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象的地址
接下来看几个案例:
由字节码指令可知s1==s2,因为编译期间就将s1="a"+"b"+"c"优化成s1="abc",s2引用的是s1的地址。(字节码解释;字节码0:ldc从字符串常量池加载字符串abc,相当于在字符串常量池放入了一个字符串abc,字节码2:将abc放入局部变量表下标为1的位置,后面字符串s2同理。下标为0的位置放的是this)
在看两个字符串拼接结果的效果对比
test3方法
可以看到编译期间的优化使用了StringBuilder
test4方法
可以看到ldc指令取出的字符串都是字符串常量池的同一个,然后分别放入局部变量表的下标3和4位置
再看以下执行的结果及原因
new String("ab")创建了几个对象?2个
new string("a") +new String("b")创建了几个对象
这里并没有在常量池里面创建"ab"
4. intern方法讲解
先看一下intern方法的api,该方法被native修饰的
intenr方法通俗解释
也就是说;当执行 String myInfo =new String("i love guigu").intern();这行代码的时候先在堆里面开辟一块内存,接着调用intern方法的时候就去字符串常量池去找找是否存在字符串"ilove guigu",如果有就直接将字符串常量池的这个"ilove guigu"的内存地址值赋予给栈里面的局部变量表(也就是说栈里面的指针指向的是stringTable的这个字符串的内存地址),如果不存在的话 就会在常量池也创建一个字符串"i love guigu",并且将常量池的这个字符串地址返回给myInfo。参考下图
再看一个例子的执行结果
上面图中代码解释:执行第一行的时候就是相当于new String("ab"),注意这里常量池并没有"ab"。
执行s.intern()就回去常量池去找看看有没有"ab",如果没有就将此堆中的"ab"地址值引用一个放入到字符串常量池,并且将常量池里面的"ab"地址被s2指向,虽然s指向的是堆里面的"ab",但是它们两的地址值是同一个。
总结下String的intern方法:
1.如果直接以字面量的方式声明一个字符串,比如String str="123";那么只会在字符串常量池创建一个"123"对象
2.当调用string的intern方法就会去字符串常量池去找有么有该对象的地址,如果没有就引用堆里面这个字符串对象的内存地址值,如果有就不会在字符串常量池里面创建重复的字符串对象。例如上图案例中s2其实就是copy了堆空间的"ab"的内存地址值,所以s也是==s2的。