1.String的基本特性
-
声明为final,不可被继承
-
实现
Serializable
接口,表示其支持序列化 -
实现
Comparable
接口,表示其可比较大小 -
JDK8内部使用
final char[] value
存储字符串数据;JDK9改为byte[]
(节约空间,因为大部分String对象都是Latin-1字符,此类字符只需要一个byte存储) -
不可变的字符序列,下面三种情况都需要重新指定内存区域赋值,不能使用原有的value进行赋值:
- 当字符串重新赋值时
String s1 = "psj"; String s2 = "psj"; s2 ="psw"; System.out.println(s1==s2); // false
- 当对现有的字符串进行拼接时,需要重新指定内存区域赋值,不能使用原有的value进行赋值
String s1 = "psj"; String s2 = "psj"; s2 += "2" System.out.println(s1); // psj System.out.println(s2); // psj2
- 使用replace方法修改字符串时
-
通过字面量给字符串赋值(即
String a = "psj"
)时,字符串值声明在堆中的字符串常量池中
String s1 = "psj";
String s2 = s1.replace('j', 'w')
System.out.println(s1); // psj
System.out.println(s2); // psw
- 字符串常量池不会存储相同内容的字符串(因为字符串常量池是一个固定大小的Hashtable)。同时为了减少Hash冲突(当放入字符串常量池的String较多时,导致链表过长,进而String.intern时性能下降),可以加大StringTable的长度
2.String的内存分配
- 对于8种基本数据类型和String类型,为了让它们运行速度更快且节省内存,都提供了常量池(类似于Java系统级别的缓存,基本数据类型的常量池由系统协调)
- JDK6及之前,字符串常量池放在永久代(分配内存小,垃圾回收频率低);JDK7/8放置在堆(分配内存大,垃圾回收频率高)
3.字符串拼接操作
- 常量和常量的拼接结果放置在常量池中(编译期优化)
// 源代码
public void test1(){
String s1 = "a" + "b";
String s2 = "ab";
System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); // true
}
// 编译后的字节码文件
public void test1() {
String s1 = "ab";
String s2 = "ab";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
- 拼接时只要有一个是变量,结果就保存在非常量池部分的堆空间中
public void test2() {
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop"; // 编译期优化
String s5 = s1 + "hadoop"; // 拼接中出现变量,则相当于在堆中new String()
String s6 = "javaEE" + s2; // 拼接中出现变量,则相当于在堆中new String()
String s7 = s1 + s2; // 拼接中出现变量,则相当于在堆中new String()
System.out.println(s3==s4); // true
System.out.println(s3==s5); // false
System.out.println(s3==s6); // false
System.out.println(s3==s7); // false
System.out.println(s5==s6); // false
System.out.println(s5==s7); // false
System.out.println(s6==s7); // false
// intern():判断字符串常量池中是否存在"javaEEhadoop"
// 存在则返回常量池中该字符串的地址
// 不存在则在常量池中加载一份该字符串并返回该对象地址
String s8 = s6.intern();
System.out.println(s3==s8); // true
}
变量拼接原理是StringBuilder(JDK8):
// 源代码:
public void test3() {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); // false
}
// 字节码文件:对于String s4 = s1 + s2
StringBuilder s4 = new StringBuilder();
s4.append("a");
s4.append("b");
s4.toString(); // 相当于new String("ab"),这个对象在非字符串常量池部分的堆空间中
注意:只有变量的拼接才是使用StringBuilder,常量或常量引用的拼接依旧使用编译期优化:
public void test4() {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); // true(s1和s2相当于常量)
}
循环进行拼接操作时,直接创建一个StringBuilder对象进行append操作比先创建一个String s = ""
再使用加号进行拼接要快许多:
后者需要创建多个StringBuilder进行append操作,并且还要进行new String(...)
操作
后者创建对象较多,占用空间大,GC时间长
4.intern()的使用
- 如果不是使用双引号声明的String对象,可调用intern方法,调用后会从字符串常量池中查询当前字符串是否存在,不存在就将该字符串放入常量池
// 以下两种方式保证变量s指向的是字符串常量池中的数据
String s = "psj";
String s = new String("psj").intern();
- 确保字符串在内存中只有一份拷贝,可以节省内存,加快字符串操作的执行速度
- JDK6和JDK7/8/11对于intern方法使用上的区别:针对常量池中没有目标字符串对象时
- JDK6会复制一份对象
- JDK7及之后会复制对象的引用地址(假设堆中已经创建了该对象,则引用堆中的对象)
String s = new String("1"); // s指向堆空间
s.intern(); // 调用该方法前,字符串常量池中已经存在"1"
String s2 = "1";
System.out.println(s == s2); // false,因为一个是堆空间中的对象,一个是字符串常量池中的对象
String s3 = new String("1") + new String("1"); // s3是堆中的地址,可以理解为new String("11"),反正最后调用toString()(不针对JDK11)
// 上述拼接代码并没有在字符串常量池中生成"11"(参考面试题3)
// *执行该行代码时:
// JDK6:常量池会创建新对象"11"
// JDK7之后:常量池不会创建"11",而是创建一个对象指向堆中生成的对象(节省空间)
s3.intern();
String s4 = "11"; // s4使用的是上一行代码执行后在常量池生成"11"的地址(该"11"在JDK6中是常量池中的,在JDK7及之后是指向堆中创建的"11"对象的地址,s3始终是指向堆中创建的"11"对象的地址)
System.out.println(s3 == s4); // JDK7之前为false,之后为true
String s3 = new String("1") + new String("1"); // 还是相当于在堆中new String("11")
String s4 = "11"; // 在字符串常量池中会生成对象"11"
s3.intern(); // 检查到在常量池中已经有"11",不创建"11"
System.out.println(s3 == s4); // false
注意:比如
new String("a") + new String("b")
,在JDK7及以上最后会调用toString()方法,该方法在字符串常量池中是没有生成"ab"的
面试题
1.
public class Test{
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public void change(String str, char[] ch){
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
Test t = new Test();
t.change(t.str, t.ch);
System.out.println(t.str); // good(不会变为test ok)
System.out.println(t.str); // best(第一个字符进行了修改)
}
}
2.字符串拼接操作中的示例
3.
String s = new String(“psj”)会创建几个对象?两个
new String(“p”) + new String(“s”)创建几个对象?JDK11中创建了4个,JDK8中创建了5个(考虑最后toString方法就是6个)
注意:这种方式在字符串常量池中最终是没有"ps"对象的,如果使用new String(“ps”)是会在常量池中有"ps"的
String s = new String("a") + new String("b"); // 拼接的方式不会在常量池中生成"ab"
String s2 = s.intern(); // JDK6会复制对象,JDK7及以上复制对象的引用
System.out.println(s == "ab");
System.out.println(s2 == "ab");
在JDK6中是true和false:
在JDK7及以上是true和true:
String x = "ab";
String s = new String("a") + new String("b"); // 还是在堆中创建"ab"对象
String s2 = s.intern(); // 因为第一行代码执行后已经在常量池中生成了"ab",所以s2直接执行常量池中已有的对象即可
System.out.println(s == "ab");
System.out.println(s2 == "ab");
在所有JDK版本都是false和true: