接上篇揭秘:Java字符串对象的内存分布原理
再看看下面几道关于String
的真实面试题,看看你废不废?
②
String str1 = "Hello";
String str2 = "He" + "llo";
String str3 = "He";
String str4 = "llo";
String str5 = str3 + str4;
String str6 = str3 + "llo";System.out.println(str1 == str2); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str5); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str6); // true or false?
③
public class StringExercise07 {public static void main(String[] args) {Test1 ex = new Test1();ex.change(ex.str, ex.ch);// 输出结果是?System.out.println(ex.str + " and ");System.out.println(ex.ch);}
}class Test1 {String str = new String("ly");final char[] ch = {'j', 'a', 'v', 'a'};public void change(String str, char ch[]) {str = "java";ch[0] = 'h';}
}
④比较下面两种字符串拼接的差异
// 第一种拼接:使用+操作符
String strPlus = "";
for (int i = 0; i < 1000; i++) {strPlus += "Hello, World!";
}// 第二种拼接:使用StringBuilder
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < 1000; i++) {strBuilder.append("Hello, World!");
}
String result = strBuilder.toString();
如果要正确回答上面几个面试题,不深入理解String
类型的内存原理是不行的,本篇的唯一目的就是言简意赅、深入浅出的梳理String
类型的底层原理。
一,字符串拼接
下面这道面试题考察的是字符串拼接的底层原理,字符串拼接可以分为两种:
- ①字面量拼接,如
String str2 = "He" + "llo";
,拼接的所有部分都是字面量,对于这种拼接,Java在编译时就会优化,将其合并为一个字符串:
// 源码:
String str2 = "He" + "llo";// 编译优化后:
String str2 = "Hello";
- ②变量拼接,
String str5 = str3 + str4;
或String str6 = str3 + "llo";
,只要有一个变量参与,就是变量拼接。对于这种拼接,JVM底层会调用StringBuilder
的append
方法进行拼接,拼接完成后调用toString
方法,toString方法中使用构造函数new String()
创建新的字符串对象
小结:
- 纯字面量拼接相当于用字面量声明一个字符串对象,字符串对象会保存到字符串常量池
- 变量拼接的相当于通过构造函数声明一个字符串对象,不会存入字符串常量池
所以,对于如下面试题:
②
String str1 = "Hello";
String str2 = "He" + "llo";
String str3 = "He";
String str4 = "llo";
String str5 = str3 + str4;
String str6 = str3 + "llo";System.out.println(str1 == str2); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str5); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str6); // true or false?
字面量拼接:
String str1 = "Hello";
String str2 = "He" + "llo";
System.out.println(str1 == str2); // true or false?
System.out.println(str1.equals(str5)); // true or false?
因为编译器的优化,代码String str2 = "He" + "llo";
相当于String str2 = "Hello";
,类似下图,在字符串常量池的作用下,变量str1
和str2
会指向同一个字符串对象:
所以其输出结果是:
变量拼接:
String str1 = "Hello";
String str2 = "He" + "llo";
String str3 = "He";
String str4 = "llo";
String str5 = str3 + str4;
String str6 = str3 + "llo";System.out.println(str1 == str5); // true or false?
System.out.println(str1.equals(str5)); // true or false?
System.out.println(str1 == str6); // true or false?
变量str5
和str6
是通过字符串变量拼接得到,底层是通过字符串构造函数创建的,所以不会通过常量池复用已经创建的对象,所以str1
和str5
两个变量指向不同地址的对象,用运算符==
比较的结果是false;但两个对象存储的字符串内容是相同的,用equals
比较的结果是true
:
二,为什么要使用StringBuilder进行字符串拼接
我们运行下面的代码,统计一下两个循环的耗时:
④比较下面两种字符串拼接的差异
long s = System.currentTimeMillis();String strPlus = "";for (int i = 0; i < 100000; i++) {strPlus += "Hello, World!";}System.out.println("字符串变量拼接耗时:" + (System.currentTimeMillis() - s));s = System.currentTimeMillis();// 第二种拼接:使用StringBuilderStringBuilder strBuilder = new StringBuilder();for (int i = 0; i < 100000; i++) {strBuilder.append("Hello, World!");}String result = strBuilder.toString();System.out.println("StringBuilder拼接耗时:" + (System.currentTimeMillis() - s));
发现第一种拼接方式耗时超过7秒,但是第二种拼接方式耗时不到3毫秒
,差距惊人。
原因在于:
- ①使用+号进行字符串变量循环拼接时,每次拼接都会创建一个StringBulider对象,并调用字符串构造函数创建一个新的字符串对象,循环次数很大时,创建大量对象耗时很长
- ②使用StringBuilder拼接对象时,是把字符串通过append方法插入到StringBuilder类的一个字符数组中,数组的插入是非常快的,尽管循环次数多,但是效率非常高,耗时极短
最佳实践:
- ①如果存在大量的字符串拼接,使用StringBuilder是更高效的方式
- ②如果拼接次数很少,比如少于10次,使用+进行拼接会使代码更简洁
所以,不必所有的字符串都用StringBuilder进行拼接。
三,扩展:就近原则
③
public class StringExercise07 {public static void main(String[] args) {Test1 ex = new Test1();ex.change(ex.str, ex.ch);// 输出结果是?System.out.println(ex.str + " and ");System.out.println(ex.ch);}
}class Test1 {String str = new String("ly");final char[] ch = {'j', 'a', 'v', 'a'};public void change(String str, char ch[]) {str = "java";ch[0] = 'h';}
}
先看下运行结果:
对于初学者,很难理解这个输出,调用了Test1
的change
方法为什么成员变量str
的值没有改变,但是变量ch
的值却改变了?
如果仔细看change
方法的代码:
public void change(String str, char ch[]) {str = "java";ch[0] = 'h';
}
change
方法的第一个参数和Test1
的成员变量str
同名,JVM在执行这个方法时,会优先从change
方法栈帧的局部变量表中查找str
变量,如果有,就不会使用Test1类的成员变量,所以Test1
的成员变量str
的值并没有被修改。
如果像下面这样修改change
的代码:
```cpp
public void change(String str, char ch[]) {this.str = "java";ch[0] = 'h';}
因为有了this关键字的作用,JVM会查找成员变量str,然后修改其值,打印结果如下:![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/028b9778c2d7422ebf5092f25a4b4f3b.png)