【JDK源碼分析】String的存儲區與不可變性


// ... literals are interned by the compiler 
// and thus refer to the same object
String s1 = "abcd";
String s2 = "abcd";
s1 == s2; // --> true 

// ... These two have the same value
// but they are not the same object
String s1 = new String("abcd");
String s2 = new String("abcd");
s1 == s2; // --> false 

看上面一段代碼,我們會發生疑惑:為什么通過字符串常量實例化的String類型對象是一樣的,而通過new所創建String對象卻不一樣呢?且看下面分解。

1. 數據存儲區

String是一個比較特殊的類,除了new之外,還可以用字面常量來定義。為了弄清楚這二者間的區別,首先我們得明白JVM運行時數據存儲區,這里有一張圖對此有清晰的描述:

非共享數據存儲區

非共享數據存儲區是在線程啟動時被創建的,包括:

  • 程序計數器(program counter register)控制線程的執行;
  • 棧(JVM Stack, Native Method Stack)存儲方法調用與對象的引用等。

共享數據存儲區

該存儲區被所有線程所共享,可分為:

  • 堆(Heap)存儲所有的Java對象,當執行new對象時,會在堆里自動進行內存分配。
  • 方法區(Method Area)存儲常量池(run-time constant pool)、字段與方法的數據、方法與構造器的代碼。

2. 兩種實例化

實例化String對象:

public class StringLiterals {
    public static void main(String[] args) {
        String one = "Test";
        String two = "Test";
        String three = "T" + "e" + "s" + "t";
        String four = new String("Test");
    }
}

javap -c StringLiterals反編譯生成字節碼,我們選取感興趣的部分如下:

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String Test
       2: astore_1
       3: ldc           #2                  // String Test
       5: astore_2
       6: ldc           #2                  // String Test
       8: astore_3
       9: new           #3                  // class java/lang/String
      12: dup
      13: ldc           #2                  // String Test
      15: invokespecial #4                  // Method java/lang/String."<init>": (Ljava/lang/String;)V
      18: astore        4
      20: return
}

ldc #2表示從常量池中取#2的常量入棧,astore_1表示將引用存在本地變量1中。因此,我們可以看出:對象onetwothree均指向常量池中的字面常量"Test";對象four是在堆中new的新對象;如下圖所示:

總結如下:

  • 當用字面常量實例化時,String對象存儲在常量池;
  • 當用new實例化時,String對象存儲在堆中;

操作符==比較的是對象的引用,當其指向的對象不同時,則為false。因此,開篇中的代碼會出現通過new所創建String對象不一樣。

3. 不可變String

String源碼

JDK7的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
}

String類被聲明為final,不可以被繼承,所有的方法隱式地指定為final,因為無法被覆蓋。字段char value[]表示String類所對應的字符串,被聲明為private final;即初始化后不能被修改。常用的new實例化對象String s1 = new String("abcd");的構造器:

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

只需將value與hash的字段值進行傳遞即可。

不可變性

所謂不可變性(immutability)指類不可以通過常用的API被修改。為了更好地理解不可變性,我們先來看《Thinking in Java》中的一段代碼:

//: operators/Assignment.java
// Assignment with objects is a bit tricky.
import static net.mindview.util.Print.*;

class Tank {
  int level;
}	

public class Assignment {
  public static void main(String[] args) {
    Tank t1 = new Tank();
    Tank t2 = new Tank();
    t1.level = 9;
    t2.level = 47;
    print("1: t1.level: " + t1.level +
          ", t2.level: " + t2.level);
    t1 = t2;
    print("2: t1.level: " + t1.level +
          ", t2.level: " + t2.level);
    t1.level = 27;
    print("3: t1.level: " + t1.level +
          ", t2.level: " + t2.level);
  }
} /* Output:
1: t1.level: 9, t2.level: 47
2: t1.level: 47, t2.level: 47
3: t1.level: 27, t2.level: 27
*///:~

上述代碼中,在賦值操作t1 = t2;之后,t1、t2包含的是相同的引用,指向同一個對象。因此對t1對象的修改,直接影響了t2對象的字段改變。顯然,Tank類是可變的。

也許,有人會說s = s.concat("ef");不是修改了對象s么?而事實上,我們去看concat的實現,會發現其返回的是新String對象(return new String(buf, true););改變的只是s1引用所指向的對象,如下圖所示:

4. 反射

String的value字段是final的,可不可以通過過某種方式修改呢?答案是反射。在stackoverflow上有這樣一段修改value字段的代碼:

String s1 = "Hello World";  
String s2 = "Hello World";  
String s3 = s1.substring(6);  
System.out.println(s1); // Hello World  
System.out.println(s2); // Hello World  
System.out.println(s3); // World  

Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s1);  
value[6] = 'J';  
value[7] = 'a';  
value[8] = 'v';  
value[9] = 'a';  
value[10] = '!';  

System.out.println(s1); // Hello Java!  
System.out.println(s2); // Hello Java!  
System.out.println(s3); // World  

在上述代碼中,為什么對象s2的值也會被修改,而對象s3的值卻不會呢?根據前面的介紹,s1與s2指向同一個對象;所以當s1被修改后,s2也會對應地被修改。至於s3對象為什么不會?我們來看看substring()的實現:

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

當beginIndex不為0時,返回的是new的String對象;當beginIndex為0時,返回的是原對象本身。如果將String s3 = s1.substring(6);改為String s3 = s1.substring(0);,那么對象s3也會被修改了。

如果仔細看java.lang.String.java,我們會發現:當需要改變字符串內容時,String類的方法返回的是新String對象;如果沒有改變,String類的方法則返回原對象引用。這節省了存儲空間與額外的開銷。

5. 參考資料

[1] Programcreek, JVM Run-Time Data Areas.
[2] Corey McGlone, Looking "Under the Hood" with javap.
[3] Programcreek, Diagram to show Java String’s Immutability.
[4] Stackoverflow, Is a Java string really immutable?
[5] Programcreek, Why String is immutable in Java ?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM