深入理解JAVA字符串類

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;
    }
    .
    .
    .
    .
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章