1. 程式人生 > >Java 乾貨之深入理解String

Java 乾貨之深入理解String

可以證明,字串操作是計算機程式設計中最常見的行為,尤其是在Java大展拳腳的Web系統中更是如此。 ---《Thinking in Java》

提到Java中的String,總是有說不完的知識點,它對於剛接觸Java的人來說,有太多太多的值得研究的東西了,可是為什麼Java中的String這麼獨特呢?今天我們來一探究竟。

基本資料型別

眾所周知Java有八大基本資料型別,那麼基本資料型別與物件有什麼異同呢?

  • 基本資料型別不是物件
  • 基本資料型別能直接儲存變數對應的值在堆疊中,存取更加高效
  • 使用方便,不用new建立,每次表示都不用新建一個物件

字面量與賦值

什麼叫字面值呢?考慮下面程式碼:

int a=3;
double d=3.32;
long l=102322332245L;

其中,3、3.32、102322332245L便叫做字面值。3預設是int型別,3.32預設是double型別,102322332245預設也是int型別,所以必須加一個L來將它修改為long型別,否則編譯器就會報錯,字面量可以直接在計算機中表示。

基本資料型別便可以直接通過字面值進行賦值

String與基本資料型別

話說了這麼多,這和String有什麼關係呢?正如本文最開始所說,因為Java需要常常與字串打交道,因此Java的設計者想要將String型別在使用上效能上儘量像基本資料型別一樣。 也就是

int i=0;
String str="test";

那麼問題來了,基本資料型別之所以叫基本資料型別,是因為這種型別可以直接在計算機中直接被表示,比如int i=0;中,0作為字面值是可以直接被表示出來"0x00",而test作為字串如何直接被表示呢?

常量池

JVM的解決方案便是Class檔案常量池。Class常量池中主要用於存放兩大類常量: 字面量符號引用量,其中字面量便包括了文字字串。

也就是當我們在類中寫下String str="test"的時候,Class檔案中,就會出現test這個字面量。

String型別的特殊性在於它只需要一個utf8表示的內容即可。 這樣便解決了String直接賦值的問題,只要在JVM中,將str與test字面量對應起來即可。

但是,問題就真的這麼簡單麼? 可別忘了,String也是一個物件,它也同時擁有所有一個物件應該擁有的特點,也就說

String str="test"

將```test```的內容寫在``` Class```檔案中僅僅解決的是如果賦值的問題,那String物件是如何在記憶體中存在呢?

----
#### String建立過程

開啟```java.lang.String```檔案,可以看到```String```擁有不可變物件的所有特點,``` final```修飾的類,``` final```修飾的成員變數,因此任何看似對String內容進行操作的方法,實際上都是返回了一個新的String物件。
正因為```String```這樣的特點,我們可以建立一個對String的物件的快取池:```String Pool```,用來快取所有第一次出現的```String```物件。 

> JVM規範中只要求了```String Pool```,但並沒有要求如何實現,在```Hot Spot JVM```中,是通過類似一個```HashSet<String>```實現,裡面儲存是當前已儲存的String物件的**引用**:

String str="test";


首先虛擬機器會在```String Pool```中查詢是否有```equals("test")```的String,如果有,就把字串常量池裡面對```"test"```物件的引用賦值給str。如果不存在,就在**堆**中新建一個"test"物件,並將引用駐留在字串常量池(```String Pool```)中,同時將該引用複製給str。

可以看到,Java在這裡是使用的String快取物件來解決“字面值”這個問題的。**也就是說,"test"所對應的字面值其實是一個在字串常量池的String物件**這樣做只要出現過一次的String物件,第二次就不再會被建立,節約了很大一筆開銷,便解決了String類似基本資料型別的效能問題。

----
#### 深入理解String

明白了String的前因後果,現在來梳理關於String的細節問題。

String str="test"


包含了3個“值”:

-  ```"test"```字面量,表示String物件所儲存的**內容**,編譯後存放在Class位元組碼中,執行時存放在```Class```物件中,而```Class```物件儲存在JVM的方法區中
-  ```test```物件,儲存在堆中
-  ```test```物件對應的引用,儲存在```String Pool```中。

如圖所示:

![image](https://raw.githubusercontent.com/dengchengchao/Images/master/3%E4%B8%AAstring.jpg)


其中

1. 一定注意str所指向的物件是存放在**堆**中的,網上大多數說的不明白,更有誤導```String Pool```中儲存的是物件的說法。Java 物件,排除逃逸分析的優化,所有物件都是儲存在堆中的。

2. ```String Pool```位於JVM 的```None Heap```中,並且```String Pool```中的引用持有對堆中對應String物件的引用,因此不必擔心堆中的String物件是被GC回收。

3. 網上很多文章還會說```test```字面值是存在```Perm Gen```中,但是這樣並不準確,永生代(“Perm Gen”)只是Sun JDK的一個實現細節而已,Java語言規範和Java虛擬機器規範都沒有規定必須有“Permanent Generation”這麼一塊空間,甚至沒規定要用什麼GC演算法——不用分代式GC演算法哪兒來的“永生代”? HotSpot的PermGen是用來實現Java虛擬機器規範中的“方法區”(method area)的。

4. 前面說過,Java想將String向基本資料型別靠近,還能體現在對```final String```物件的處理,對於```final String```,如果使用僅僅是字面值的作用,而並沒有涉及到物件操作的話(使用物件訪問操作符"."),編譯器會直接將對應的值替換為相應字面值。舉例:
 
    
    ```
    final String str="hello";
    String helloWorld=str+"world";
    ```
    編譯器會直接優化:
    ```
    String helloWorld="helloworld";
    ```
 
    
    ```
    final String str="hello";
    String hello=String.valueOf(str); 
    ```
    編譯器會直接優化
    ```
    String hello=String.valueOf("hello"); 
    ```
    如果沒有編譯器的優化,就會涉及到運算元壓棧出棧等操作,但是經過優化後的String,可以發現並不會有astore/aload等指令的出現.

-----

#### new String()
其實```new String```沒什麼好說的,```new String()```表示將```String```完全作為一個物件來看,放棄它的基本資料型別性質,也與```String Pool```沒有任何關係,但是```String```包含的```intern()```方法能將它與```String Pool```關聯起來。

- jdk 1.7之前,```intern()```表示若```String Pool```中不存在該字串,則 在堆中新建一個與呼叫```intern()```物件的字面值相同的物件,並在```String Pool```中儲存該物件的引用,同時返回該物件,若存在則直接返回。
- jdk 1.7及1.7 之後,```intern()```表示將呼叫```intern()```物件的引用直接複製一份到```String Pool```中。

網上很多討論涉及到幾個物件

String str=new String("hello world");

下面圖解分析:

![image](https://raw.githubusercontent.com/dengchengchao/Images/master/newString.jpg)

> 需要明白的一點的是```new String("hello world")```並不是一個原子操作,可以將其分為兩步,每個關鍵字負責不同的工作其中```new```負責生成物件,```String("hello world")```負責初始化```new```
生成的物件。

- 首先,執行```new```操作,在**堆**中分配空間,並生成一個```String```物件。
- 其次,將```new```生成的物件的引用傳遞給```String("hello world")```方法進行初始化,而此時引數中出現了```"hello world"```字面量,JVM會先在**字串常量池**裡面檢查是否有```equals("hello world")```的引用,如果沒有,就在**堆**中建立相應的物件,並生成一個引用指向這個物件,並將此引用儲存在```字串常量池```中。

2. 再次,複製常量池```hello world```指向的**字面量**物件傳遞給``` new String("hello world")```進行初始化。

> 第二點中提到了複製,其實最主要的就是複製```String```物件中```value```所指向的地址,也就是將方法區中的```"hello world"```的索引複製給新的物件,這也是為什麼上圖中,兩個物件都指向方法區中同一個位置

下面的```String str=new String("hello world")```進行反編譯的結果:
   0: new           #2                  // class java/lang/String
   3: dup
   4: ldc           #3                  // String hello world
   6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
   9: astore_1
  10: return
大概的指令應該都能看到,解釋一下:

- new 執行new 操作,在堆中分配記憶體
- dup 將new 操作生成的物件壓棧
- ldc 將String型常量值從常量池中推送至棧頂
- invokespecial 呼叫```new String()```並傳入new 出來的物件了ldc的String值

> ldc指令是什麼東西?
簡單地說,它用於將int、float或String型常量值從常量池中推送至棧頂,在這裡也能看到,JVM是將```String```和八大基本資料型別統一處理的。    
> ldc 還隱藏了一個操作:也就是"hello world"的resolve操作,也就是檢測“hello world”是否已經在常量池中存在的操作。
>傳送門詳見:[Java 中new String("字面量") 中 "字面量" 是何時進入字串常量池的?](https://www.zhihu.com/question/55994121)

-----

有個很神奇的坑,《深入理解JVM》中曾經提到過這個問題,不過周志明老師是拿的"java"作為舉例:
    程式碼如下(jdk 1.7)
    ```
    
    public class RuntimeConstantPoolOOM {
        public static void main(String[] args) {
            String str1 = new StringBuilder("計算機").append("軟體").toString();
            System.out.println(str1.intern() == str1);
    
            String str2 = new StringBuilder("ja").append("va").toString();
            System.out.println(str2.intern() == str2);
        }
    }
    ```
    結果 
    true
    false
    
不明白為什麼"java"字串會在執行StringBuilder.toString()之前出現過?   
其實是因為:Java 標準庫在JVM啟動的時候載入的某些類已經包含了```java```字面量。

傳送門:[如何理解《深入理解java虛擬機器》第二版中對String.intern()方法的講解中所舉的例子?](https://www.zhihu.com/question/51102308)

-----
#### 方法區
上面圖中說了,```“hello wold”```物件的```value```的值是放在方法區中的。如何證明呢?
這裡我們可以使用反射來幹一些壞事。   
雖然```String```類是一個不可變物件,對外並沒有提供如何修改```value```的方法,但是反射可以。
    String str1=new String("abcd");
    String str2="abcd";
    String str3="abcd";
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);//設定訪問許可權
    char[] value = (char[]) valueField.get(str2);
    value[0] = '0';
    value[1] = '1';
    value[2] = '2';
    value[3] = '3';

    System.out.println(str1);
    System.out.println(str2);
    System.out.println(str3);
    String str4="abcd";
    System.out.println(str4);
    System.out.println("abcd");
可以試一試,輸出結果都是```0123```,因為在編譯的時候生成```Class```物件的時候,```str1,str2,str3,str4```都是指向的```Class```檔案中同一個位置,而在執行的時候這個```Class```物件的值被修改後,所有和```abcd```有關的物件的```value```都會被改變。   
相信理解了這一個例子的同學,能夠對```String```有一個更加深刻的理解

-----
#### 檢驗
說了這麼多,你真的懂了麼?來看看經常出現的一些關於```String```的問題:

1.

String str1 = new StringBuilder("Hel").append("lo").toString(); System.out.println(str1.intern() == str1); String str = "Hello"; System.out.println(str == str1);

2.

String str1="hello"; String str2=new String("hello"); System.out.println(str2 == str1);

3.

final String str1="hell"; String str2="hello"; String str3=str1+"o"; System.out.println(str2 == str3);

4.

String str1="hell"; String str2="hello"; String str3=str1+"o"; System.out.println(str2 == str3); ```