Java中字符串類主要包含3種:String、StringBuilder、StringBuffer,今天我們就從源碼、性能和使用場景等角度來深入分析Java中的字符串類。
1. String
String類提供了構造和管理字符串的各種基本操作,Java中所有類似"WSYN"的字面值都是String類的實例。以下是String類的部分源碼:
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
它是典型的Immutable(不可修改的)類,被聲明爲final class,這意味着String類不能被繼承,並且所有屬性也都是final的。由於String類是不可變性,類似拼接、裁剪字符串的操作都會產生新的String對象,所以字符串操作不當可能會產生大量臨時字符串。JDK 8以前String類是使用char(字符)數組來存儲字符串的,JDK 9以後改爲byte(字節)數組存儲。
1.1 常量池
在分析常量池前先看下java的內存區域:
在Java的內存區域中有三種常量池,分別是:Class常量池、運行時常量池和字符串常量池。
1.1.1 Class常量池
每一個Java文件被java編譯器編譯後都會生成一個Class字節碼文件,Class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有常量池,用於存放編譯期生成的各種字面量和符號引用。字面量相當於Java語言層面常量的概念,如文本字符串,聲明爲final的常量值等,符號引用則屬於編譯原理方面的概念,包括瞭如下三種類型的常量:
- 類和接口的全限定名
- 字段名稱和描述符
- 方法名稱和描述符
每個類都有一個Class常量池。
1.1.2 運行時常量池
運行時常量池存在於內存中的方法區。Class常量池被加載到內存之後會存放在運行時常量池中。除了保存Class文件中的符號引用外,還會將符號引用轉爲直接引用,並存儲在運行時常量池中。
運行時常量池相對於Class常量池另一個特性就是動態性。除了在編譯期產生的Class文件中常量池的內容會進入運行時常量池,運行期間也可能產生新的常量放入池中,如String類的intern()方法。
JVM在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內存中後,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在解析階段,會把符號引用替換爲直接引用,解析的過程會去查詢字符串常量池,以保證運行時常量池所引用的字符串與字符串常量池中是一致的。
1.1.3 字符串常量池
字符串常量池有兩種使用方式:
(1)直接使用雙引號聲明出來的String對象會直接存儲在常量池中;
String str = "wsyn";
(2)如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中(JDK 7後將字符串在堆的引用地址放入常量池)。
String str2 = new String("wsyn");
str2 = str2.intern();
Java中創建字符串的兩種方式:
(1)直接使用雙引號聲明一個字符串時,JVM首先查找字符串常量池是否存在該字符串,若存在則直接返回該字符串的引用地址;若不存在則先在字符串常量池實例化該字符串,再返回引用地址。
String str = "wsyn";
(2)通過new關鍵字新建一個字符串時,會在堆中重新new一塊內存,用於存儲字符串信息,同時會在棧中創建對堆的地址引用(JDK 7後字符串堆的地址引用從棧中轉到字符串常量池)。
String str2 = new String("wsyn");
字符串常量池的位置
JDK7以後字符串常量池從方法區的永久代中移入到堆中。因爲方法區的內存空間太小且不方便擴展,而堆的內存空間比較大且擴展方便。
字符串常量池的內部結構
- 在HotSpot VM裏實現的string pool功能的是一個StringTable類,它是一個Hash表,默認值大小長度是1009;這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。字符串常量由一個一個字符組成,放在了StringTable上。
- 在JDK1.6中,StringTable的長度是固定的,長度就是1009,因此如果放入String Pool中的String非常多,就會造成hash衝突,導致鏈表過長,當調用String#intern()時會需要到鏈表上一個一個找,從而導致性能大幅度下降;
- 在JDK1.7中,StringTable的長度可以通過參數指定:
-XX:StringTableSize=11111
1.3 == 和 equals區別以及hash衝突
- ==是比較兩個字符串的地址引用是否一致。
- equals是判斷兩個字符串內容是否相等。具體邏輯爲先判斷兩個字符串是否==,即是否指向同一個引用地址,如果引用地址一樣,則表示是同一個對象,內容一定相等;若地址不一樣,則逐一比較是否相等。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
那麼問題來了,是否可以通過hashCode來比較兩個字符串是否一致?
答案是否定的,兩個字符串==或者equals,他們的hashCode肯定是一樣的,反之不然。先看下hashCode的源碼:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
由上可以看出,hashCode的計算方式爲:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],s[n] 是字符串的第 n個字符,n 是字符串的長度,^ 表示求冪。由此可知不同字符串有可能會得出相同的hashCode,雖然概率很低。
2. StringBuffer和StringBuilder
StringBuffer和StringBuilder底層都是利用可修改的char字符數組(JDK 9以後改爲byte字節數組 ),爲了實現修改字符序列的目的;同時他們都繼承了AbstractStringBuilder,區別僅在於最終的方法是否加了synchronized。
那麼這個內部數組的長度爲多大合適呢?目前,內部數組長度爲構建時初始字符串+16,這就意味着如果沒有輸入初始字符串,那麼初始長度就是16。如果字符串長度值這個長度,就會進行擴容,重新創建新的足夠大的數組,進行arraycopy,拋棄掉原來的數組,會產生多重開銷。所以在我們能夠預計字符串長度,可以指定合適大小。
2.1 StringBuffer
StringBuffer是爲了解決上述提到的String類拼接產生太多中間對象的問題而提供的一個類,可以通過append或者add方法,把字符串添加到已有序列的末尾或指定位置。
StringBuffer本質是一個線程安全的可修改序列,它的線程安全是通過把各種修改數據的方法都加上synchronized關鍵字實現的,由此也帶來了額外的性能開銷,所以非線程安全的需要,一般不推薦使用。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
private transient char[] toStringCache;
/** use serialVersionUID from JDK 1.0.2 for interoperability */
static final long serialVersionUID = 3388685877147921107L;
public StringBuffer() {
super(16);
}
public StringBuffer(int capacity) {
super(capacity);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
public StringBuffer(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
@Override
public synchronized void trimToSize() {
super.trimToSize();
}
@Override
public synchronized void setLength(int newLength) {
toStringCache = null;
super.setLength(newLength);
}
@Override
public synchronized char charAt(int index) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
return value[index];
}
.
.
.
.
}
2.2 StringBuilder
StringBuilder 是 JDK 5 中新增的,在能力上和 StringBuffer 沒有本質區別,但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字符串拼接的首選。
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/** use serialVersionUID for interoperability */
static final long serialVersionUID = 4383685877147921099L;
public StringBuilder() {
super(16);
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
.
.
.
.
}