在學習Java的過程當中,咱們會被告知 String 被設計成不可變的類型。爲何 String 會被 Java 開發者有如此特殊的對待?他們的設計意圖和設計理念究竟是什麼?所以,我帶着如下三個問題,對
String 進行剖析:java
String 真的不可變嗎?數據庫
爲何會將 String 設計爲不可變?編程
如何經過技術實現實現 String 不可變 ?數組
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 //other codes }
String 的底層實現是依靠 char[] 數組,既然依靠的是基礎類型變量,那麼他必定是可變的, String 之因此不可變,是由於 Java 的開發者經過技術實現,隔絕了使用者對 String 的底層數據的操做。可是,咱們能夠同反射的機制,操做 String 的底層,檢驗其不可變的猜測。安全
反射的方式操做 String :併發
//建立字符串"Hello World", 並賦給引用s String s = "Hello World"; System.out.println("s = " + s); // Hello World //獲取String類中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改變value屬性的訪問權限 valueFieldOfString.setAccessible(true); //獲取s對象上的value屬性的值 char[] value = (char[]) valueFieldOfString.get(s); //改變value所引用的數組中的第5個字符 value[5] = '_'; System.out.println("s = " + s); //Hello_World
經過兩次字符串的輸出,咱們能夠看到,String 被改變了,可是在代碼裏,幾乎不會使用反射的機制去操做 String 字符串,因此,咱們會認爲 String 類型是不可變的。app
安全socket
引起安全問題,譬如,數據庫的用戶名、密碼都是以字符串的形式傳入來得到數據庫的鏈接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。由於字符串是不可變的,因此它的值是不可改變的,不然黑客們能夠鑽到空子,改變字符串指向的對象的值,形成安全漏洞性能
保證線程安全,在併發場景下,多個線程同時讀寫資源時,會引競態條件,因爲 String 是不可變的,不會引起線程的問題而保證了線程
HashCode,當 String 被建立出來的時候,hashcode也會隨之被緩存,hashcode的計算與value有關,若 String 可變,那麼 hashcode 也會隨之變化,針對於 Map、Set 等容器,他們的鍵值須要保證惟一性和一致性,所以,String 的不可變性使其比其餘對象更適合當容器的鍵值。
性能
當字符串是不可變時,字符串常量池纔有意義。字符串常量池的出現,能夠減小建立相同字面量的字符串,讓不一樣的引用指向池中同一個字符串,爲運行時節約不少的堆內存。若字符串可變,字符串常量池失去意義,基於常量池的String.intern()方法也失效,每次建立新的 String 將在堆內開闢出新的空間,佔據更多的內存
實例代碼:
String 的不可變性:
public static String appendStr(String s){ s+="bbb"; return s; } //可變的StringBuilder public static StringBuilder appendSb(StringBuilder sb){ return sb.append("bbb"); } public static void main(String[] args){ //String作參數 String s=new String("aaa"); String ns=Test.appendStr(s); System.out.println("String aaa >>> "+s.toString()); // aaa //StringBuilder作參數 StringBuilder sb=new StringBuilder("aaa"); StringBuilder nsb=Test.appendSb(sb); System.out.println("StringBuilder aaa >>> "+sb.toString()); // aaabbb }
打開JDK的源碼:
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 //other codes }
String 類由關鍵字 final 修飾,說明該類不可繼承
char value[] 屬性也被 final 所修飾,說明 value 的引用在建立以後,就不能被改變
以上兩點並不能徹底實現 String 不可變 ,緣由在於:
final int[] value={1,2,3} int[] another={4,5,6}; value=another; // 編譯器報錯,final不可變
value 被 final 修飾,只能保證引用不被改變,可是 value 所指向的堆中的數組,纔是真實的數據,只要可以操做堆中的數組,依舊能改變數據。【解釋:String其實是可變的】
final int[] value={1,2,3}; value[2]=100; //這時候數組裏已是{1,2,100}
全部的成員屬性均被 private 關鍵字所修飾
爲了實現 String 不可變,關鍵在於Java的開發者在設計和開發 String 的過程當中,沒有暴露任何的內部成員,與此同時 API 的設計是均沒有操做 value 的值 , 而是採用 new String() 的方式返回新的字符串,保證了 String 的不可變。
JDK String API 源碼:
public static String valueOf(char c) { char data[] = {c}; return new String(data, true); //採用 new String() 的方式返回新的字符串 } public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); //採用 new String() 的方式返回新的字符串 }
整個String設成final禁止繼承,避免被其餘人繼承後破壞。因此String是不可變的關鍵都在底層的實現,而不是一個final。考驗的是工程師構造數據類型,封裝數據的功力。
String s = "abcd"; s = "abcdel";
String 不可變性的圖示: