你有沒有想過: 爲何Java中String是不可變的?

有一種學得快的方法,就是一次不要學太多。
public final class String implements Serializable, Comparable<String>, CharSequence {
    private final char[] value; // 用 private final 修飾的字符數組存儲字符串
    private int hash;
    private static final long serialVersionUID = -6849794470754667710L;
    
    public String() {
        this.value = "".value; 
    }

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

    public String(char[] var1) {
        this.value = Arrays.copyOf(var1, var1.length);
    }
    ......
}

解答

有三點:
1)String 在底層是用一個 private final 修飾的字符數組 value 來存儲字符串的。final 修飾符保證了 value 這個引用變量是不可變的,private 修飾符則保證了 value 是類私有的,不能經過對象實例去訪問和更改 value 數組裏存放的字符。java

注:有不少地方說 String 不可變是 final 起的做用,其實不嚴謹。由於即便我不用 final 修改 value ,但初始化完成後我能保證之後都不更改 value 這個引用變量和 value[] 數組裏存放的值,它也是從沒變化過的。final 只是保證了 value 這個引用變量是不能更改的,但不能保證 value[] 數組裏存放的字符是不能更改的。若是把 private 改成 public 修飾,String類的對象是能夠經過訪問 value 去更改 value[] 數組裏存放的字符的,這時 String 就再也不是不可變的了。因此不如說 private 起的做用更大一些。後面咱們會經過 代碼1處 去驗證。

2)String 類並無對外暴露能夠修改 value[] 數組內容的方法,而且 String 類內部對字符串的操做和改變都是經過新建一個 String 對象去完成的,操做完返回的是新的 String 對象,並無改變原來對象的 value[] 數組。面試

注:String 類若是對外暴露能夠更改 value[] 數組的方法,如 setter 方法,也是不能保證 String 是不可變的。後面咱們會經過 代碼2處 去驗證。

3)String 類是用 final 修飾的,保證了 String 類是不能經過子類繼承去破壞或更改它的不可變性的。數組

注:若是 String 類不是用 final 修飾的,也就是 String 類是能夠被子類繼承的,那子類就能夠改變父類原有的方法或屬性。後面咱們會經過 代碼3處 去驗證。

以上三個條件同時知足,才讓 String 類成了不可變類,才讓 String 類具備了一旦實例化就不能改變它的內容的屬性。數據結構

面試問題:String 類是用什麼數據結構來存儲字符串的?
由上面 String 的源碼可見,String 類是用數組的數據結構來存儲字符串的ide

代碼1處:

咱們來看看若是把 private 修飾符換成 public,看看會發生什麼?學習

// 先來模擬一個String類,初始化的時候將 String 轉成 value 數組存儲
public final class WhyStringImutable {
   public final char[] value;  // 修飾符改爲了 public 
   
   public WhyStringImutable() {
       this.value = "".toCharArray();
   }
   
   public WhyStringImutable(String str){
       this.value = str.toCharArray(); // 初始化時轉爲字符數組
   }
   
   public char[] getValue(){
       return this.value;
   }
}
public class WhyStringImutableTest {
    public static void main(String[] args) {
        WhyStringImutable str = new WhyStringImutable("abcd");
        System.out.println("原str中value數組的內容爲:");
        System.out.println(str.getValue()); // 打印str對象中存放的字符數組
        System.out.println("----------");
        str.value[1] = 'e'; // 經過對象實例訪問value數組並修改其內容
        System.out.println("修改後str中value數組的內容爲:");
        System.out.println(str.getValue()); // 打印str對象中存放的字符數組
   }
}

輸出結果:this

原str中value數組的內容爲:
abcd
----------
修改後str中value數組的內容爲:
aecd

因而可知,private 修改成 public 後,String 是能夠經過對象實例訪問並修改所保存的value 數組的,並不能保證 String 的不可變性。設計

代碼2處:

咱們若是對外暴露能夠更改 value[] 數組的方法,如 setter 方法,看看又會發生什麼?code

public final class WhyStringImutable {
    private final char[] value;

    public WhyStringImutable() {
        this.value = "".toCharArray();
    }

    public WhyStringImutable(String str){
        this.value = str.toCharArray();
    }
    
    // 對外暴露能夠修改 value 數組的方法
    public void setValue(int i, char ch){
        this.value[i] = ch;
    }
    
    public char[] getValue(){
        return this.value;
    }

}
public class WhyStringImutableTest {
    public static void main(String[] args) {
        WhyStringImutable str = new WhyStringImutable("abcd");
        System.out.println("原str中value數組的內容爲:");
        System.out.println(str.getValue()); // 打印str對象中存放的字符數組
        System.out.println("----------");
        str.setValue(1,'e'); // 經過set方法改變指定位置的value數組元素
        System.out.println("修改後str中value數組的內容爲:");
        System.out.println(str.getValue()); // 打印str對象中存放的字符數組
   }
}

輸出結果:對象

原str中value數組的內容爲:
abcd
----------
修改後str中value數組的內容爲:
aecd

因而可知,若是對外暴露了能夠更改 value[] 數組內容的方法,也是不能保證 String 的不可變性的。

代碼3處:

若是 WhyStringImutable 類去掉 final 修飾,其餘的保持不變,又會怎樣呢?

public class WhyStringImutable {
    private final char[] value;
    
    public WhyStringImutable() {
        this.value = "".toCharArray();
    }
    
    public WhyStringImutable(String str){
        this.value = str.toCharArray(); // 初始化時轉爲字符數組
    }
    
    public char[] getValue(){
        return this.value;
    }
}

寫一個子類繼承自WhyStringImutable 並修改原來父類的屬性,實現子類本身的邏輯:

public class WhyStringImutableChild extends WhyStringImutable {

    public char[] value; // 修改字符數組爲 public 修飾,不要 final 

    public WhyStringImutableChild(String str){
        this.value = str.toCharArray();
    }

    public WhyStringImutableChild() {
        this.value = "".toCharArray();
    }

    @Override
    public char[] getValue() {
        return this.value;
    }
}
public class WhyStringImutableTest {
    public static void main(String[] args) {
        WhyStringImutableChild str = new WhyStringImutableChild("abcd");
        System.out.println("原str中value數組的內容爲:");
        System.out.println(str.getValue());
        System.out.println("----------");
        str.value[1] = 's';
        System.out.println("修改後str中value數組的內容爲:");
        System.out.println(str.getValue());
    }
}

運行結果:

原str中value數組的內容爲:
abcd
----------
修改後str中value數組的內容爲:
ascd

因而可知,若是 String 類不是用 final 修飾的,是能夠經過子類繼承來修改它原來的屬性的,因此也是不能保證它的不可變性的。

總結

綜上所分析,String 不可變的緣由是 JDK 設計者巧妙的設計瞭如上三點,保證了String 類是個不可變類,讓 String 具備了不可變的屬性。考驗的是工程師構造數據類型,封裝數據的功力,而不是簡單的用 final 來修飾,背後的設計思想值得咱們理解和學習。

拓展

從上面的分析,咱們知道,String 確實是個不可變的類,但咱們就真的沒辦法改變 String 對象的值了嗎?不是的,經過反射能夠改變 String 對象的值

可是請謹慎那麼作,由於一旦經過反射改變對應的 String 對象的值,後面再建立相同內容的 String 對象時都會是反射改變後的值,這時候在後面的代碼邏輯執行時就會出現讓你 「摸不着頭腦」 的現象,具備迷惑性,出了奇葩的問題你也很難排除到緣由。後面在 代碼4處 咱們會驗證這個問題。

先來看看如何經過反射改變 String 對象的內容:

public class WhyStringImutableTest {
    public static void main(String[] args) {
        String str = new String("123");
        System.out.println("反射前 str:"+str);
        try {
            Field field = String.class.getDeclaredField("value");
            field.setAccessible(true);
            char[] aa = (char[]) field.get(str);
            aa[1] = '1';
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        System.out.println("反射後 str:"+str);
}

打印結果:

反射前 str:123
反射後 str:113 // 可見,反射後,str 的值確實改變了

代碼4處:

下面咱們來驗證由於一旦經過反射改變對應的 String 對象的值,後面再建立相同內容的 String 對象時都會是反射改變後的值的問題:

public class WhyStringImutableTest {
    public static void main(String[] args) {
        String str = new String("123");
        System.out.println("反射前 str:"+str);
        try {
            Field field = String.class.getDeclaredField("value");
            field.setAccessible(true);
            char[] aa = (char[]) field.get(str);
            aa[1] = '1';
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        System.out.println("反射後 str:"+str);
        
        String str2 = new String("123");
        System.out.println("str2:"+str2); // 咱們來看 str2 會輸出什麼,會輸出 113?
        System.out.println("判斷是不是同一對象:"+(str == str2)); // 判斷 str 和 str2 的內存地址值是否相等
        System.out.println("判斷內容是否相同:"+str.equals(str2)); // 判斷 str 和 str2 的內容是否相等
}

執行結果以下:

反射前 str:123
反射後 str:113
str2:113 // 居然不是123??而是輸出113,說明 str2 也是反射修改後的值。
判斷是不是同一對象:false // 輸出 false,說明在內存中確實建立了兩個不一樣的對象
判斷內容是否相同:true   // 輸出true,說明依然判斷爲兩個對象內容是相等的

由上面的輸出結果,咱們可知,反射後再新建相同內容的字符串對象時會是反射修改後的值,這就形成了很大迷惑性,在實際開發中要謹慎這麼作。

相關文章
相關標籤/搜索