摘要:有一種學得快的方法,就是一次不要學太多。用修飾的字符數(shù)組存儲字符串解答有三點在底層是用一個修飾的字符數(shù)組來存儲字符串的。修飾符保證了這個引用變量是不可變的,修飾符則保證了是類私有的,不能通過對象實例去訪問和更改數(shù)組里存放的字符。
有一種學得快的方法,就是一次不要學太多。
public final class String implements Serializable, Comparable解答:, CharSequence { private final char[] value; // 用 private final 修飾的字符數(shù)組存儲字符串 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 修飾的字符數(shù)組 value 來存儲字符串的。final 修飾符保證了 value 這個引用變量是不可變的,private 修飾符則保證了 value 是類私有的,不能通過對象實例去訪問和更改 value 數(shù)組里存放的字符。
注:有很多地方說 String 不可變是 final 起的作用,其實不嚴謹。因為即使我不用 final 修改 value ,但初始化完成后我能保證以后都不更改 value 這個引用變量和 value[] 數(shù)組里存放的值,它也是從沒變化過的。final 只是保證了 value 這個引用變量是不能更改的,但不能保證 value[] 數(shù)組里存放的字符是不能更改的。如果把 private 改為 public 修飾,String類的對象是可以通過訪問 value 去更改 value[] 數(shù)組里存放的字符的,這時 String 就不再是不可變的了。所以不如說 private 起的作用更大一些。后面我們會通過 代碼1處 去驗證。
2)String 類并沒有對外暴露可以修改 value[] 數(shù)組內容的方法,并且 String 類內部對字符串的操作和改變都是通過新建一個 String 對象去完成的,操作完返回的是新的 String 對象,并沒有改變原來對象的 value[] 數(shù)組。
注:String 類如果對外暴露可以更改 value[] 數(shù)組的方法,如 setter 方法,也是不能保證 String 是不可變的。后面我們會通過 代碼2處 去驗證。
3)String 類是用 final 修飾的,保證了 String 類是不能通過子類繼承去破壞或更改它的不可變性的。
注:如果 String 類不是用 final 修飾的,也就是 String 類是可以被子類繼承的,那子類就可以改變父類原有的方法或屬性。后面我們會通過 代碼3處 去驗證。
以上三個條件同時滿足,才讓 String 類成了不可變類,才讓 String 類具有了一旦實例化就不能改變它的內容的屬性。
面試問題:String 類是用什么數(shù)據(jù)結構來存儲字符串的?
由上面 String 的源碼可見,String 類是用數(shù)組的數(shù)據(jù)結構來存儲字符串的。
我們來看看如果把 private 修飾符換成 public,看看會發(fā)生什么?
// 先來模擬一個String類,初始化的時候將 String 轉成 value 數(shù)組存儲 public final class WhyStringImutable { public final char[] value; // 修飾符改成了 public public WhyStringImutable() { this.value = "".toCharArray(); } public WhyStringImutable(String str){ this.value = str.toCharArray(); // 初始化時轉為字符數(shù)組 } 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數(shù)組的內容為:"); System.out.println(str.getValue()); // 打印str對象中存放的字符數(shù)組 System.out.println("----------"); str.value[1] = "e"; // 通過對象實例訪問value數(shù)組并修改其內容 System.out.println("修改后str中value數(shù)組的內容為:"); System.out.println(str.getValue()); // 打印str對象中存放的字符數(shù)組 } }
輸出結果:
原str中value數(shù)組的內容為: abcd ---------- 修改后str中value數(shù)組的內容為: aecd
由此可見,private 修改為 public 后,String 是可以通過對象實例訪問并修改所保存的value 數(shù)組的,并不能保證 String 的不可變性。
代碼2處:我們如果對外暴露可以更改 value[] 數(shù)組的方法,如 setter 方法,看看又會發(fā)生什么?
public final class WhyStringImutable { private final char[] value; public WhyStringImutable() { this.value = "".toCharArray(); } public WhyStringImutable(String str){ this.value = str.toCharArray(); } // 對外暴露可以修改 value 數(shù)組的方法 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數(shù)組的內容為:"); System.out.println(str.getValue()); // 打印str對象中存放的字符數(shù)組 System.out.println("----------"); str.setValue(1,"e"); // 通過set方法改變指定位置的value數(shù)組元素 System.out.println("修改后str中value數(shù)組的內容為:"); System.out.println(str.getValue()); // 打印str對象中存放的字符數(shù)組 } }
輸出結果:
原str中value數(shù)組的內容為: abcd ---------- 修改后str中value數(shù)組的內容為: aecd
由此可見,如果對外暴露了可以更改 value[] 數(shù)組內容的方法,也是不能保證 String 的不可變性的。
代碼3處:如果 WhyStringImutable 類去掉 final 修飾,其他的保持不變,又會怎樣呢?
public class WhyStringImutable { private final char[] value; public WhyStringImutable() { this.value = "".toCharArray(); } public WhyStringImutable(String str){ this.value = str.toCharArray(); // 初始化時轉為字符數(shù)組 } public char[] getValue(){ return this.value; } }
寫一個子類繼承自WhyStringImutable 并修改原來父類的屬性,實現(xiàn)子類自己的邏輯:
public class WhyStringImutableChild extends WhyStringImutable { public char[] value; // 修改字符數(shù)組為 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數(shù)組的內容為:"); System.out.println(str.getValue()); System.out.println("----------"); str.value[1] = "s"; System.out.println("修改后str中value數(shù)組的內容為:"); System.out.println(str.getValue()); } }
運行結果:
原str中value數(shù)組的內容為: abcd ---------- 修改后str中value數(shù)組的內容為: ascd
由此可見,如果 String 類不是用 final 修飾的,是可以通過子類繼承來修改它原來的屬性的,所以也是不能保證它的不可變性的。
總結綜上所分析,String 不可變的原因是 JDK 設計者巧妙的設計了如上三點,保證了String 類是個不可變類,讓 String 具有了不可變的屬性??简灥氖枪こ處煒嬙鞌?shù)據(jù)類型,封裝數(shù)據(jù)的功力,而不是簡單的用 final 來修飾,背后的設計思想值得我們理解和學習。
拓展從上面的分析,我們知道,String 確實是個不可變的類,但我們就真的沒辦法改變 String 對象的值了嗎?不是的,通過反射可以改變 String 對象的值。
但是請謹慎那么做,因為一旦通過反射改變對應的 String 對象的值,后面再創(chuàng)建相同內容的 String 對象時都會是反射改變后的值,這時候在后面的代碼邏輯執(zhí)行時就會出現(xiàn)讓你 “摸不著頭腦” 的現(xiàn)象,具有迷惑性,出了奇葩的問題你也很難排除到原因。后面在 代碼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 對象的值,后面再創(chuàng)建相同內容的 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 的內容是否相等 }
執(zhí)行結果如下:
反射前 str:123 反射后 str:113 str2:113 // 竟然不是123??而是輸出113,說明 str2 也是反射修改后的值。 判斷是否是同一對象:false // 輸出 false,說明在內存中確實創(chuàng)建了兩個不同的對象 判斷內容是否相同:true // 輸出true,說明依然判斷為兩個對象內容是相等的
由上面的輸出結果,我們可知,反射后再新建相同內容的字符串對象時會是反射修改后的值,這就造成了很大迷惑性,在實際開發(fā)中要謹慎這么做。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://www.ezyhdfw.cn/yun/73357.html
摘要:所有變量的類型在編譯時已知在程序運行之前,因此編譯器也可以推導出所有表達式的類型。像變量的類型一樣,這些聲明是重要的文檔,對代碼讀者很有用,并由編譯器進行靜態(tài)檢查。對象類型的值對象類型的值是由其類型標記的圓。 大綱 1.編程語言中的數(shù)據(jù)類型2.靜態(tài)與動態(tài)數(shù)據(jù)類型3.類型檢查4.易變性和不變性5.快照圖6.復雜的數(shù)據(jù)類型:數(shù)組和集合7.有用的不可變類型8.空引用9.總結 編程語言中的數(shù)據(jù)...
摘要:與都繼承自類,在中也是使用字符數(shù)組保存字符串,,這兩種對象都是可變的。采用字節(jié)碼的好處語言通過字節(jié)碼的方式,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題,同時又保留了解釋型語言可移植的特點。 String和StringBuffer、StringBuilder的區(qū)別是什么?String為什么是不可變的? String和StringBuffer、StringBuilder的區(qū)別 可變性...
摘要:性能當字符串是不可變時,字符串常量池才有意義。字符串常量池的出現(xiàn),可以減少創(chuàng)建相同字面量的字符串,讓不同的引用指向池中同一個字符串,為運行時節(jié)約很多的堆內存。 在學習Java的過程中,我們會被告知 String 被設計成不可變的類型。為什么 String 會被 Java 開發(fā)者有如此特殊的對待?他們的設計意圖和設計理念到底是什么?因此,我?guī)е韵氯齻€問題,對 String 進行剖析: ...
摘要:性能,大量運用在哈希的處理中,由于的不可變性,可以只計算一次哈希值,然后緩存在內部,后續(xù)直接取就好了。這是目前的一個底層字節(jié)碼的實現(xiàn),那么是不是沒有使用或者的必要了呢。 凱倫說,公眾號ID: KailunTalk,努力寫出最優(yōu)質的技術文章,歡迎關注探討。 1. 前言 最近看到幾個有趣的關于Java核心類String的問題。 String類是如何實現(xiàn)其不可變的特性的,設計成不可變的好處...
摘要:原文出自本文總結了程序員常犯的個錯誤??梢钥纯礊槭裁丛谥斜辉O計成不可變父類和子類的構造函數(shù)以上這段代碼出現(xiàn)編譯錯誤,因為默認的父類構造函數(shù)未定義。如果程序員定義構造函數(shù),編譯器將不插入默認的無參數(shù)構造函數(shù)。 原文出自:http://www.programcreek.com/2014/05/top-10-mistakes-java-developers-make/ 本文總結了J...
閱讀 850·2021-11-22 13:54
閱讀 3214·2021-09-26 10:16
閱讀 3652·2021-09-08 09:35
閱讀 1658·2019-08-30 15:55
閱讀 3505·2019-08-30 15:54
閱讀 2162·2019-08-30 10:57
閱讀 564·2019-08-29 16:25
閱讀 948·2019-08-29 16:15