Java雜記17—String全面解析

冰洋發表於2018-05-28

前言

基於字串String在java中的地位,關於String的常識性知識就不多做介紹了,我們先來看一段程式碼

public class Test {
    public static void main(String[] args) {
        String a = "abc";
        String b = "abc";
        String c = new String("abc");
        System.out.println(a==b);
        System.out.println(a.equals(b));
        System.out.println(a==c);
        System.out.println(a.equals(c));
    }
}
複製程式碼

那麼上段程式碼的結果是什麼呢?答案是:true true false true,有初學java的朋友肯定會納悶,a==c為什麼會是false呢?equals判斷的為什麼都是true呢?

根據這些問題,我們就通過對String的解讀來一步一步的瞭解。

為什麼a==c的結果是false

明白這個問題需要對JVM的記憶體結構有一定的瞭解,說是瞭解也不需要太多,能夠get到下圖的知識點就行了。

ps:本文中所有的圖示均是為了方便理解,畫出來的大致樣子,如果想要了解的更加清楚,請自行研究虛擬機器原理。

Stringpool記憶體演示

java語法設計的時候針對String,提供了兩種建立方式和一種特殊的儲存機制(String intern pool )。

兩種建立字串物件的方式:

  1. 字面值的方式賦值
  2. new關鍵字新建一個字串物件

這兩種方法在效能和記憶體佔用方面存在這差異

String Pool串池:是在記憶體堆中專門劃分一塊空間,用來儲存所有String物件資料,當構造一個新字串String物件時(通過字面量賦值的方法),Java編譯機制會優先在這個池子裡查詢是否已經存在能滿足需要的String物件,如果有的話就直接返回該物件的地址引用(沒有的話就正常的構造一個新物件,丟進去存起來),這樣下次再使用同一個String的時候,就可以直接從串池中取,不需要再次建立物件,也就避免了很多不必要的空間開銷。

根據以上的概念,我們再來看前言中的程式碼,當JVM執行到String a = "abc";的時候,會先看常量池裡有沒有字串剛好是“abc”這個物件,如果沒有,在常量池裡建立初始化該物件,並把引用指向它,如下圖。

String1

當執行到String b = "abc";時,發現常量池已經有了abc這個值,於是不再在常量池中建立這個物件,而是把引用直接指向了該物件,如下圖:

String2

繼續執行到 String c = new String("abc");這時候我們加了一個new關鍵字,這個關鍵字呢就是告訴JVM,你直接在堆記憶體裡給我開闢一塊新的記憶體,如下圖所示:

String3

這時候我們執行四個列印語句,我們需要知道==比較的是地址,equals比較的是內容(String中的重寫過了),abc三個變數的內容完全一樣,因此equals的結果都是true,ab是一個同一個物件,因此地址一樣,a和c很顯然不是同一個物件,那麼此時為false也是很好理解的。

String相關原始碼

在本文中只有String的部分原始碼,畢竟String的原始碼有3000多行,全部來寫進來不那麼現實,我們挑一些比較有意思的程式碼來做一定的分析說明。

屬性

我們先來看一下String都有哪些成員變數,比較關鍵的屬性有兩個,如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    char陣列
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
複製程式碼

從原始碼中我們能夠看到,在String類中宣告瞭一個char[]陣列,變數名value,宣告瞭一個int型別的變數hash(該String物件的雜湊值的快取)。也就是說java中的String類其實就是對char陣列的封裝。

構造方法

接下來我們通過一句程式碼來了解一下字串建立的過程,String c = new String("abc");我們知道使用new關鍵字就會使用到構造方法,所以如下。

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
複製程式碼

構造方法中的程式碼非常簡單,把傳進來的字串的value值,也就是char陣列賦值給當前物件,hash同樣處理,那麼問題來了WTF original?

在這裡需要注意的是java中的一個機制,在Java中,當值被雙引號引起來(如本示例中的"abc"),JVM會去先檢檢視一看常量池裡有沒有abc這個物件,如果沒有,把abc初始化為物件放入常量池,如果有,直接返回常量池內容。所以也就是說在沒有“abc”的基礎上,執行程式碼會在串池中建立一個abc,也會在堆記憶體中再new出來一個。最終的結果如下圖:

String4

那麼這時候如果再有一個String c2 = new String("abc");呢?如圖

String5

關於這一點我們通過IDEA的debug功能也能夠看到,你會發現,c和c2其中的char陣列的地址是相同的。足以說明在建立c和c2的時候使用的是同一個陣列。

String6

equals方法

 public boolean equals(Object anObject) {
     //如果兩個物件是同一個引用,那麼直接返回true
        if (this == anObject) {
            return true;
        }
     /*
     1.判斷傳入的物件是不是String型別
     2.判斷兩個物件的char陣列長度是否一致
     3.迴圈判斷char陣列中的每一個值是否相等
     以上條件均滿足才會返回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;
    }

複製程式碼

為什麼String不可變?

串池需要

為什麼說是串池需要呢?在開篇的時候我們提到過,串池中的字串會被多個變數引用,這樣的機制讓字串物件得到了複用,避免了很多不必要的記憶體消耗。

那麼大家試想一下,如果String物件本身允許二次修改的話,我有一個字串“abc”同時被100個變數引用,其中一個引用修改了String物件,那麼將會影響到其他99個引用該物件的變數,這樣會對其他變數造成不可控的影響。

不可變性的優點

安全性

字串不可變安全性的考慮處於兩個方面,資料安全和執行緒安全。

資料安全,大家可以回憶一下,我們都在哪些地方大量的使用了字串?網路資料傳輸,檔案IO等,也就是說當我們在傳參的時候,使用不可變類不需要去考慮誰可能會修改其內部的值,如果使用可變類的話,可能需要每次記得重新拷貝出裡面的值,效能會有一定的損失。

執行緒安全,因為字串是不可變的,所以是多執行緒安全的,同一個字串例項可以被多個執行緒共享,這樣便不用因為執行緒安全問題而使用同步。

效能效率

關於效能效率一方面是複用,另一方面呢需要從hash值的快取方向來說起了。

String的Hash值在很多的地方都會被使用到,如果保證了String的不可變性,也就能夠保證Hash值始終也是不可變的,這樣就不需要在每次使用的時候重新計算hash值了。

String不可變性是如何實現的?

通過對屬性私有化,final修飾,同時沒有提供公開的get set方法以及其他的能夠修改屬性的方法,保證了在建立之後不會被從外部修改。

同時不能忘了,String也是被final修飾的,在之前的文章中我們提到過,final修飾類的結果是String類沒有子類。

那麼String真的不能改變嗎?不是,通過反射我們可以,程式碼如下:

String c = new String("abc");
System.out.println(c);
//獲取String類中的value欄位
Field valueFieldOfString = String.class.getDeclaredField("value");

//改變value屬性的訪問許可權
valueFieldOfString.setAccessible(true);

//獲取s物件上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(c);

//改變value所引用的陣列中的第5個字元
value[1] = '_';
System.out.println(c);
複製程式碼

執行的結果是

abc
a_c
複製程式碼

也就是說我們改變了字串物件的值,有什麼意義呢?沒什麼意義,我們從來不會這麼做。

其他問題

不是特別需要請不要使用new關鍵字建立字串

從前文我們知道使用new關鍵字建立String的時候,即便串池中存在相同String,仍然會再次在堆記憶體中建立物件,會浪費記憶體,另一方面物件的建立相較於從串池中取效率也更低下。

String StringBuffer StringBuilder的區別

關於三者的區別,在面試題中經常的出現,String物件不可變,因此在進行任何內容上的修改時都會建立新的字串物件,一旦修改操作太多就會造成大量的資源浪費。

StringBuffer和StringBuilder在進行字串拼接的時候不會建立新的物件,而是在原物件上修改,不同之處在於StringBuffer執行緒安全,StringBuilder執行緒不安全。所以在進行字串拼接的時候推薦使用StringBuffer或者StringBuilder。


我不能保證每一個地方都是對的,但是可以保證每一句話,每一行程式碼都是經過推敲和斟酌的。希望每一篇文章背後都是自己追求純粹技術人生的態度。

永遠相信美好的事情即將發生。

相關文章