JAVA中String的底層解析

JAVA中String 是Final類不能被繼承。JAVA 對String的處理和一般Class有所不同。
這文章主要是解釋一下String的存儲模式和java的字符串常量池的機制,和幾個涉及底層的引用問題解析。

首先提出幾個問題:
1.String的內容爲什麼是不可更改的?
2.JAVA中“adc”這種創建的字符串的創建過程是怎樣的?
3.String(String string)的構造方法是如何工作的?
4.一個線程中內容爲“adc”的String對象,存儲的char[]是否是同一個,char[]數組是否一定在字符串常量池中?
5思考java中String 不可更改的好處在哪?
6 intern方法和字符串常量池的關係?
7string的+編譯器是如何處理的?

1.String的內容爲什麼是不可更改的?
我們通過源代碼可以看到存儲string內容的char[]是這麼定義的:

private final char value[];

可能有人會有疑問既然是final引用卻沒有附初始值。
答案是final變量是可以在構造方法中進行賦值的。
所以value的所有賦值都在String的幾個構造方法中。
這樣從代碼邏輯上控制了String不可變。

2.JAVA中“adc”這種創建的字符串的創建過程是怎樣的?

這個問題比較簡單,就是”adc“會被放到字符串常量池中,可以稱爲字面量,所有String s=“adb” 的字符串的引用都是指向字符串常量池中的。

3.String(String string)的構造方法是如何工作的?

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

這說明從用String去new String構造新的對象是,確實會產生新的對象,而且新的value和hash值都和原String對象一致。
當然String還是有其他構造方法,都會去new char[]

4一個線程中內容爲“adc”的String對象,存儲的char[]是否是同一個,char[]數組是否一定在字符串常量池中?
答案其實可以根據3推導出。
由String產生的String裏面的char[]是同一個,其他方式產生的都是新的。“”包裹產生的字符串會在常量池中,其他的都是正常的存在堆中。所以堆中可以有n份“adb”的串,常量池中的“adc”永遠只有一個,可以被多個引用所指向。後續將會用代碼解釋上述所有現象。

public static void main(String[] args)
			throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
		// TODO Auto-generated method stub

		String string = "abc";
		String string2 = "abc";
	    String string3 = new String(string);
	    char[] charString={'a','b','c'};
		String string4 = new String(charString);
		String string5 = new String(string4);
		
		charString[2]='e';
	
		Class sClass = String.class;
		Field privateStringField = sClass.getDeclaredField("value");
		privateStringField.setAccessible(true);
		char[] chars = (char[]) privateStringField.get(string);
		char[] chars2 = (char[]) privateStringField.get(string2);
		char[] chars3 = (char[]) privateStringField.get(string3);
		char[] chars4 = (char[]) privateStringField.get(string4);
		char[] chars5 = (char[]) privateStringField.get(string5); 
		
		System.out.println("++" + chars + "       " + Arrays.toString(chars));
		System.out.println("++" + chars2 + "       " + Arrays.toString(chars2));
		System.out.println("++" + chars3 + "       " + Arrays.toString(chars3));
		System.out.println("++" + chars4 + "        " + Arrays.toString(chars4));
		System.out.println("++" + chars5 + "       " + Arrays.toString(chars5));
		System.out.println("++" + charString + "       " + Arrays.toString(charString));
		System.out.println(string==string2);
		System.out.println(string3==string);
		System.out.println(string4==string);
		System.out.println(string5==string4);
		
		

	}

控制檯輸出:

++[C@4e25154f       [a, b, c]
++[C@4e25154f       [a, b, c]
++[C@4e25154f       [a, b, c]
++[C@70dea4e        [a, b, c]
++[C@70dea4e        [a, b, c]
++[C@5c647e05      [a, b, e]
true
false
false
false

我們用反射可以拿到char[] 的引用,並打印出char[] 指向的內存地址。
可見前三個String都是指向同一個地址的(字符串常量池),後三個String都是指向堆中的地址。

5思考java中String 不可更改的好處在哪?
1.如果可變複用將變得不穩定。複用可以節約內存
2.hashcode被緩存,沒必要重複結算
3.線程安全(網上的說法,我並不完全贊同,能保證值得安全,並不能保證引用的安全總是)
4.如果定義爲final 也就是String的引用和內容都會穩定不可變(當然不包括使用反射的情況)

6intern方法的作用?
intern的作用是 將string放入常量池,並返回該引用,如果已經在常量池中直接返回引用。
在JDK1.6之後 intern方法是將不存在字符串常量池的String去記錄到常量池裏(存的是引用)實例還是在堆上
在JDK1.6之前intern方法是把String複製到字符串常量池裏是新對象了(據說是在永久代中)目前可以確定String實例肯定不是一個了,裏面的char[]是否複用,這個還不確定,手裏沒有JDK16的環境,可以用我上面的反射方法來進行驗證。

7string的+編譯器是如何處理的?
兩種情況:
如果只是“” +“”+“”+… 不涉及變量的,javac會直接合並所有的"" “” ,形成utf8 結構體,保存下來,結構體中有個2byte的length字段記錄字面量(”“這種東西官方稱爲字面量)在類運行中會把結構體的東西加載到內存的字符串常量池中(具體加載細節後續再聊)
第二種情況:

        String j1="a";
	    String j2="b";
	    String j12="ab";
	    String j3="a"+j12;
	    System.out.println(j3 == j12); (輸出是false)

遇到這種情況javac並不會合併”“ ”“,儘管j12也是字面量的變量。我們可以通過javap -c查看字節碼:

Code:
       0: ldc           #2                  // String a
       2: astore_2
       3: ldc           #3                  // String b
       5: astore_3
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: ldc           #2                  // String a
      15: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      18: aload_3
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore        4

很清楚的看到,這裏會new StringBuilder 調用append方法,最後toString。StringBulider的toString方法我們可以看一下,是根據當前的char[]去 newString的,所以必然不是用一個對象,所以輸出的結果是false。

未完待續…

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