String类
由于我们每天都在和String打交道,他已经重要的让我们无法感知它的存在,正因如此,他的性能问题也最容易被忽视。下面让我们从源码的角度来看看,String类有哪些奥秘。(以下代码使用jdk1.8)
1. 存储结构
首先可以看到,String存储结构是char数组。
private final char value[];
2. 不可变
可以看到,String类本身以及其属性都是final的,为什么呢?
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
- 如果String类本身不是final,那么很可能通过继承而被修改。
- hash属性不更改确保了唯一性。
- value不变是为了实现字符串常量池,避免相同字符串的重复创建
这里可能有一个误区,刚学习Java的同学可能会说,下面的代码没有问题,变量a的值变了啊???
String a = "abc";
a = "efg";
在Java中,一切都是引用,类似于c语言中的指针。这里面a就是一个引用,而“abc”,“efg”是放在堆中的常量池的。在上面的代码中,a一开始指向常量池中的“abc”,后来指向了“efg”,“abc”还在常量池中呆着,并没有变,等待下一轮GC,被垃圾收集处理掉。
在这里,如果将a声明为final的话,说明这个引用是不可变的,上面的代码就出问题了。
3. String对象的优化
1) 字符串拼接
在阿里巴巴Java开发手册1.4节17条明确规定,在循环体内的连接方式要使用StringBuilder的append方法进行拓展。
其实在编译期,编译器会对代码进行优化,自动拼接字符串常量。那么对于变量,则优化为StringBuilder方式,但是问题来了,在循环体内,就会生成n个StringBuilder对象,代码如下。
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = str + i;
}
// 编译优化
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}
所以,在循环体内一定要用显示StringBuilder的方式拼接字符串,提升性能。
2) intern方法
我们可以看到intern是个本地方法。
public native String intern();
前面我们说到,String类的存储不可变性是为了实现常量池,实现字符串的复用。在每次赋值的时候使用intern方法,如果常量池中有相同的值,就返回常量池中字符串对象的引用,如果没有,就把字符串的引用添加到常量池中,返回常量池中的字符串引用。
举个栗子,“abc”是常量,在编译期就放入了常量池。在运行时,a指向常量池中的字符串引用。new String会在堆中创建一个String对象,并返回String对象的引用。当调用了intern方法时,c会到常量池中查找是否有相同的字符串,有则返回它的引用。
String a = "abc";
String b = new String("abc");
String c = new String("abc").intern();
所以a和c都指向常量池中的同一个字符串,而b指向堆中的String对象。但是三者的value属性都是指向同一个char数组的。如下图: