摘要:有一種學得快的方法,就是一次不要學太多。用修飾的字符數組存儲字符串解答有三點在底層是用一個修飾的字符數組來存儲字符串的。修飾符保證了這個引用變量是不可變的,修飾符則保證了是類私有的,不能通過對象實例去訪問和更改數組里存放的字符。
有一種學得快的方法,就是一次不要學太多。
public final class String implements Serializable, Comparable解答:, 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 數組里存放的字符。
注:有很多地方說 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 類是用數組的數據結構來存儲字符串的。
我們來看看如果把 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對象中存放的字符數組 } }
輸出結果:
原str中value數組的內容為: abcd ---------- 修改后str中value數組的內容為: aecd
由此可見,private 修改為 public 后,String 是可以通過對象實例訪問并修改所保存的value 數組的,并不能保證 String 的不可變性。
代碼2處:我們如果對外暴露可以更改 value[] 數組的方法,如 setter 方法,看看又會發生什么?
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,說明依然判斷為兩個對象內容是相等的
由上面的輸出結果,我們可知,反射后再新建相同內容的字符串對象時會是反射修改后的值,這就造成了很大迷惑性,在實際開發中要謹慎這么做。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73357.html
摘要:所有變量的類型在編譯時已知在程序運行之前,因此編譯器也可以推導出所有表達式的類型。像變量的類型一樣,這些聲明是重要的文檔,對代碼讀者很有用,并由編譯器進行靜態檢查。對象類型的值對象類型的值是由其類型標記的圓。 大綱 1.編程語言中的數據類型2.靜態與動態數據類型3.類型檢查4.易變性和不變性5.快照圖6.復雜的數據類型:數組和集合7.有用的不可變類型8.空引用9.總結 編程語言中的數據...
摘要:與都繼承自類,在中也是使用字符數組保存字符串,,這兩種對象都是可變的。采用字節碼的好處語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。 String和StringBuffer、StringBuilder的區別是什么?String為什么是不可變的? String和StringBuffer、StringBuilder的區別 可變性...
摘要:性能當字符串是不可變時,字符串常量池才有意義。字符串常量池的出現,可以減少創建相同字面量的字符串,讓不同的引用指向池中同一個字符串,為運行時節約很多的堆內存。 在學習Java的過程中,我們會被告知 String 被設計成不可變的類型。為什么 String 會被 Java 開發者有如此特殊的對待?他們的設計意圖和設計理念到底是什么?因此,我帶著以下三個問題,對 String 進行剖析: ...
摘要:性能,大量運用在哈希的處理中,由于的不可變性,可以只計算一次哈希值,然后緩存在內部,后續直接取就好了。這是目前的一個底層字節碼的實現,那么是不是沒有使用或者的必要了呢。 凱倫說,公眾號ID: KailunTalk,努力寫出最優質的技術文章,歡迎關注探討。 1. 前言 最近看到幾個有趣的關于Java核心類String的問題。 String類是如何實現其不可變的特性的,設計成不可變的好處...
摘要:原文出自本文總結了程序員常犯的個錯誤。可以看看為什么在中被設計成不可變父類和子類的構造函數以上這段代碼出現編譯錯誤,因為默認的父類構造函數未定義。如果程序員定義構造函數,編譯器將不插入默認的無參數構造函數。 原文出自:http://www.programcreek.com/2014/05/top-10-mistakes-java-developers-make/ 本文總結了J...
閱讀 704·2021-11-22 13:54
閱讀 3065·2021-09-26 10:16
閱讀 3490·2021-09-08 09:35
閱讀 1576·2019-08-30 15:55
閱讀 3429·2019-08-30 15:54
閱讀 2076·2019-08-30 10:57
閱讀 497·2019-08-29 16:25
閱讀 877·2019-08-29 16:15