深入理解Java String類(綜合)

在Java語言了中,所有類似“ABC”的字面值,都是String類的實例;String類位於java.lang包下,是Java語言的核心類,提供了字符串的比較、查找、截取、大小寫轉換等操作;Java語言爲“+”連接符(字符串連接符)以及對象轉換爲字符串提供了特殊的支持,字符串對象可以使用“+”連接其他對象。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
    ...
}

 從上面可以看出
1)String類被final關鍵字修飾,意味着String類不能被繼承,並且它的成員方法都默認爲final方法;字符串一旦創建就不能再修改。
2)String類實現了Serializable、CharSequence、 Comparable接口。
3)String實例的值是通過字符數組實現字符串存儲的。

1. “+”連接符
    1.1 “+”連接符的實現原理
    1.2 “+”連接符的效率

2. 字符串常量池
    2.1 內存區域
    2.2 存放的內容

3. intern方法
    3.1 intern的用法

4. String、StringBuilder和StringBuffer
    4.1 繼承結構
    4.2 主要區別

 

1. “+”連接符
1.1 “+”連接符的實現原理

Java語言爲“+”連接符以及對象轉換爲字符串提供了特殊的支持,字符串對象可以使用“+”連接其他對象。其中字符串連接是通過 StringBuilder(或 StringBuffer)類及其append 方法實現的,對象轉換爲字符串是通過 toString 方法實現的,該方法由 Object 類定義,並可被 Java 中的所有類繼承。有關字符連接和轉換的更多信息,可以參閱 Gosling、Joy 和 Steele 合著的 《The Java Language Specification》

我們可以通過反編譯驗證一下

/**
 * 測試代碼
 */
public class Test {
    public static void main(String[] args) {
        int i = 10;
        String s = "abc";
        System.out.println(s + i);
    }
}

/**
 * 反編譯後
 */
public class Test {
    public static void main(String args[]) {    //刪除了默認構造函數和字節碼
        byte byte0 = 10;      
        String s = "abc";      
        System.out.println((new StringBuilder()).append(s).append(byte0).toString());
    }
}

 由上可以看出,Java中使用”+”連接字符串對象時,會創建一個StringBuilder()對象,並調用append()方法將數據拼接,最後調用toString()方法返回拼接好的字符串。由於append()方法的各種重載形式會調用String.valueOf方法,所以我們可以認爲:

//以下兩者是等價的
s = i + ""
s = String.valueOf(i);

//以下兩者也是等價的
s = "abc" + i;
s = new StringBuilder("abc").append(i).toString();

 

1.2 “+”連接符的效率

使用“+”連接符時,JVM會隱式創建StringBuilder對象,這種方式在大部分情況下並不會造成效率的損失,不過在進行大量循環拼接字符串時則需要注意。

String s = "abc";
for (int i=0; i<10000; i++) {
    s += "abc";
}

/**
 * 反編譯後
 */
String s = "abc";
for(int i = 0; i < 1000; i++) {
     s = (new StringBuilder()).append(s).append("abc").toString();    
}

 這樣由於大量StringBuilder創建在堆內存中,肯定會造成效率的損失,所以在這種情況下建議在循環體外創建一個StringBuilder對象調用append()方法手動拼接(如上面例子如果使用手動拼接運行時間將縮小到1/200左右)。

/**
 * 循環中使用StringBuilder代替“+”連接符
 */
StringBuilder sb = new StringBuilder("abc");
for (int i = 0; i < 1000; i++) {
    sb.append("abc");
}
sb.toString();

 與此之外還有一種特殊情況,也就是當”+”兩端均爲編譯期確定的字符串常量時,編譯器會進行相應的優化,直接將兩個字符串常量拼接好,例如:

System.out.println("Hello" + "World");

/**
 * 反編譯後
 */
System.out.println("HelloWorld");

 

/**
 * 編譯期確定
 * 對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的字節碼流中。
 * 所以此時的"a" + s1和"a" + "b"效果是一樣的。故結果爲true。
 */
String s0 = "ab"; 
final String s1 = "b"; 
String s2 = "a" + s1;  
System.out.println((s0 == s2)); //result = true

 

/**
 * 編譯期無法確定
 * 這裏面雖然將s1用final修飾了,但是由於其賦值是通過方法調用返回的,那麼它的值只能在運行期間確定
 * 因此s0和s2指向的不是同一個對象,故上面程序的結果爲false。
 */
String s0 = "ab"; 
final String s1 = getS1(); 
String s2 = "a" + s1; 
System.out.println((s0 == s2)); //result = false 

public String getS1() {  
    return "b";   
}

 綜上,“+”連接符對於直接相加的字符串常量效率很高,因爲在編譯期間便確定了它的值,也就是說形如”I”+”love”+”java”; 的字符串相加,在編譯期間便被優化成了”Ilovejava”。對於間接相加(即包含字符串引用,且編譯期無法確定值的),形如s1+s2+s3; 效率要比直接相加低,因爲在編譯器不會對引用變量進行優化。


2. 字符串常量池

在Java的內存分配中,總共3種常量池,分別是Class常量池、運行時常量池、字符串常量池。

字符串的分配和其他對象分配一樣,是需要消耗高昂的時間和空間的,而且字符串使用的非常多。JVM爲了提高性能和減少內存的開銷,在實例化字符串的時候進行了一些優化:使用字符串常量池。每當創建字符串常量時,JVM會首先檢查字符串常量池,如果該字符串已經存在常量池中,那麼就直接返回常量池中的實例引用。如果字符串不存在常量池中,就會實例化該字符串並且將其放到常量池中。由於String字符串的不可變性,常量池中一定不存在兩個相同的字符串。

/**
 * 字符串常量池中的字符串只存在一份!
 * 運行結果爲true
 */
String s1 = "hello world!";
String s2 = "hello world!";
System.out.println(s1 == s2);

 

2.1 內存區域

在HotSpot VM中字符串常量池是通過一個StringTable類實現的,它是一個Hash表,默認值大小長度是1009;這個StringTable在每個HotSpot VM的實例中只有一份,被所有的類共享;字符串常量由一個一個字符組成,放在了StringTable上。要注意的是,如果放進String Pool的String非常多,就會造成Hash衝突嚴重,從而導致鏈表會很長,而鏈表長了後直接會造成的影響就是當調用String.intern時性能會大幅下降(因爲要一個一個找)。

在JDK6及之前版本,字符串常量池是放在Perm Gen區(也就是方法區)中的,StringTable的長度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的長度可以通過-XX:StringTableSize=66666參數指定。至於JDK7爲什麼把常量池移動到堆上實現,原因可能是由於方法區的內存空間太小且不方便擴展,而堆的內存空間比較大且擴展方便。


2.2 存放的內容

在JDK6及之前版本中,String Pool裏放的都是字符串常量;在JDK7.0中,由於String.intern()發生了改變,因此String Pool中也可以存放放於堆內的字符串對象的引用。

/**
 * 運行結果爲true false
 */
String s1 = "AB";
String s2 = "AB";
String s3 = new String("AB");
System.out.println(s1 == s2);
System.out.println(s1 == s3);

 由於常量池中不存在兩個相同的對象,所以s1和s2都是指向JVM字符串常量池中的”AB”對象。new關鍵字一定會產生一個對象,並且這個對象存儲在堆中。所以String s3 = new String(“AB”);產生了兩個對象:保存在棧中的s3和保存堆中的String對象。

當執行String s1 = “AB”時,JVM首先會去字符串常量池中檢查是否存在”AB”對象,如果不存在,則在字符串常量池中創建”AB”對象,並將”AB”對象的地址返回給s1;如果存在,則不創建任何對象,直接將字符串常量池中”AB”對象的地址返回給s1。

3. intern方法

直接使用雙引號聲明出來的String對象會直接存儲在字符串常量池中,如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern 方法是一個native方法,intern方法會從字符串常量池中查詢當前字符串是否存在,如果存在,就直接返回當前字符串;如果不存在就會將當前字符串放入常量池中,之後再返回。

JDK1.7的改動:
1. 將String常量池 從 Perm 區移動到了 Java Heap區
2. String.intern() 方法時,如果存在堆中的對象,會直接保存對象的引用,而不會重新創建對象。

/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code true}.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java™ Language Specification</cite>.
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 */
public native String intern();

 

 3.1 intern的用法

static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {
    Integer[] DB_DATA = new Integer[10];
    Random random = new Random(10 * 10000);
    for (int i = 0; i < DB_DATA.length; i++) {
        DB_DATA[i] = random.nextInt();
    }
    long t = System.currentTimeMillis();
    for (int i = 0; i < MAX; i++) {
        //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
         arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
    }

    System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();
}

 運行的參數是:-Xmx2g -Xms2g -Xmn1500M 上述代碼是一個演示代碼,其中有兩條語句不一樣,一條是未使用 intern,一條是使用 intern。結果如下圖

未使用intern,耗時826ms:

使用intern,耗時2160ms:

通過上述結果,我們發現不使用 intern 的代碼生成了1000w 個字符串,佔用了大約640m 空間。 使用了 intern 的代碼生成了1345個字符串,佔用總空間 133k 左右。其實通過觀察程序中只是用到了10個字符串,所以準確計算後應該是正好相差100w 倍。雖然例子有些極端,但確實能準確反應出 intern 使用後產生的巨大空間節省。

細心的同學會發現使用了 intern 方法後時間上有了一些增長。這是因爲程序中每次都是用了 new String 後,然後又進行 intern 操作的耗時時間,這一點如果在內存空間充足的情況下確實是無法避免的,但我們平時使用時,內存空間肯定不是無限大的,不使用 intern 佔用空間導致 jvm 垃圾回收的時間是要遠遠大於這點時間的。 畢竟這裏使用了1000w次intern 纔多出來1秒鐘多的時間。


4. String、StringBuilder和StringBuffer
4.1 繼承結構


4.2 主要區別

1)String是不可變字符序列,StringBuilder和StringBuffer是可變字符序列。
2)執行速度StringBuilder > StringBuffer > String。
3)StringBuilder是非線程安全的,StringBuffer是線程安全的。


5. 總結

String類是我們使用頻率最高的類之一,也是面試官經常考察的題目,下面是一個小測驗。

public static void main(String[] args) {
    String s1 = "AB";
    String s2 = new String("AB");
    String s3 = "A";
    String s4 = "B";
    String s5 = "A" + "B";
    String s6 = s3 + s4;
    System.out.println(s1 == s2);
    System.out.println(s1 == s5);
    System.out.println(s1 == s6);
    System.out.println(s1 == s6.intern());
    System.out.println(s2 == s2.intern());
}

 
解析:真正理解此題目需要清楚以下三點
1)直接使用雙引號聲明出來的String對象會直接存儲在常量池中;
2)String對象的intern方法會得到字符串對象在常量池中對應的引用,如果常量池中沒有對應的字符串,則該字符串將被添加到常量池中,然後返回常量池中字符串的引用;
3) 字符串的+操作其本質是創建了StringBuilder對象進行append操作,然後將拼接後的StringBuilder對象用toString方法處理成String對象,這一點可以用javap -c命令獲得class文件對應的JVM字節碼指令就可以看出來。

 本文參考:https://blog.csdn.net/ifwinds/article/details/80849184

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