Java字符串原理分析之String是否可變
一. Spring源碼中的final關鍵詞
為了弄清楚String為什么具有不可變性,我們先來看看String的源碼,尤其是源碼中帶有final關鍵詞的地方。
1. final的特點
為了更好地理解String相關的內容,在閱讀String源碼之前,我們先來復習一下final關鍵詞有哪些特點,因為在String中會涉及到很多final相關的內容。
final關鍵詞修飾的類不可以被其他類繼承,但是該類本身可以繼承其他類,通俗的說就是這個類可以有父類,但是不能有子類;
final關鍵詞修飾的方法不可以被覆蓋重寫,但是可以被繼承使用;
final關鍵詞修飾的基本數據類型變量稱為常量,只能被賦值一次;
final關鍵詞修飾的引用數據類型的變量值為地址值,地址值不能改變,但是地址內的數據對象可以被改變;
final關鍵詞修飾的成員變量,需要在創(chuàng)建對象前賦值,否則會報錯(即需要在定義時直接賦值,如果是在構造方法中賦值,則多個構造方法均需賦值)。
復習了final的特點之后,接下來我們就可以閱讀String的源碼了。
2. String源碼解讀
接下來就請大家請跟著小編來看看String源碼中關于不可變性的內容吧。
2.1 final修飾的String類
/** * ......其他略...... * * Strings are constant; their values cannot be changed after they * are created. String buffers support mutable strings. * Because String objects are immutable they can be shared. For example: * * ......其他略...... */ public final class String implements java.io.Serializable, Comparable<String>, CharSequence { ......
先對上面的源碼及其注釋進行簡單的解釋:
- final:請參考第1小節(jié)對final特點的介紹;
- Serializable:用于序列化;
- Comparable:默認的比較器;
- CharSequence: 提供對字符序列進行統(tǒng)一、只讀的操作。
從這一段源碼及注釋中,我們可以得出如下結論:
- String類用final關鍵字修飾,說明String不可被繼承;
- String字符串是常量,字符串的值一旦被創(chuàng)建,就不能被改變;
- String字符串緩沖區(qū)支持可變字符串;
- 因為String對象是不可變的,所以它們是可以被共享的。
2.2 final修飾的value[]屬性
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ......
從源碼中可以看出,value[]是一個私有的字符數組,String類其實就是通過這個char數組來保存字符串內容的。簡單的說,我們定義的字符串會被拆成一個一個的字符,這些字符都被存放在這個value字符數組里面。
這里的value[]數組被final修飾,初始化之后就不能再被更改。但是大家注意,我們這里說的value[]不可變,指的是value的引用地址不可變,但是value數組里面的數據元素其實是可變的! 這是因為value是數組類型,根據我們之前學過的知識,value的引用地址會分配在棧中 ,而其對應的數據是在常量池中保存的。所以我們說String不可變,指的就是value在棧中的引用地址不可變,而不是說常量池中數組本身的數據元素不可變。
另外我們要注意,Java中的字符串常量池,用來存儲字符串字面量! 但是由于JDK版本的不同,常量池的位置也不同:
JDK 6 及以下版本的字符串常量池是在方法區(qū)(Perm Gen)中,此時常量池中存儲的是字符串對象;在 JDK 8.0 中,方法區(qū)(永久代被元空間取代了;
JDK 7、8以后的字符串常量池被轉移到了堆中,此時常量池存儲的就是字符串對象的引用,而不是字符串對象本身。
至此,就帶各位把String類中的核心源碼分析完了,接下來我們再進一步分析String不可變的原因,及其他底層原理設計。
二. String的不可變性
1. 實驗案例
了解了上面的這些核心源碼之后,接下來再帶各位來驗證一下,看看String到底能不能變!我先給各位來一段案例代碼,代碼案例如下圖所示。
結果s的內容變了,好像是啪啪打臉了????。?!咋回事,不是說了String不可變嗎?怎么這么快就翻車打臉了?別急,讓我們好好來分析一下。
2. 結果剖析
首先我們從結果上來看String s 變量的結果好像改變了,但為什么我們又說String是不可變的呢?
要想明白這個問題,我們得先弄清楚一個點,即引用和值的區(qū)別!在上面的代碼中,我們先是創(chuàng)建了一個 "yiyige" 為內容的字符串引用s,s其實先是指向了value對象,而value對象則指向存儲了 "y,i,y,i,g,e" 字符的字符數組。因為value被final修飾,所以value的值不可被更改。因此,上面代碼中改變的其實是s的引用指向,而不是改變了String對象的值 。 換句話說,上面實例中 s的值 只是 value的引用地址,并不是String內容本身!當我們執(zhí)行 s = "yyg"; 語句時,Java中會創(chuàng)建一個新的字面量對象 "yyg",而原來的 "yiyige" 字面量對象依然存在于內存的intern緩存池中。 在Java中,因為數組也是對象, 所以value中存儲的也只是一個引用,它指向一個真正的數組對象。在執(zhí)行了String s = “yiyige”; 這句代碼之后,真正的內存布局應該是下圖這樣的:
因為value是String封裝的字符數組,value中的所有字符都屬于String這個對象。由于value是private的,且沒有提供setValue等公共方法來修改這個value值,所以在String類的外部是無法修改value值的,也就是說一旦初始化就不能被修改。此外,value變量是final的, 也就是說在String類內部,一旦這個值初始化了,value這個變量所引用的地址就不會改變了,即一直引用同一個對象。正是基于這一層,所以說String對象是不可變的對象。 但其實value所引用對象的內容完全可以發(fā)生改變,我們可以利用反射來消除String類對象的不可變特性 。
所以String的不可變性,指的是value在棧中的引用地址不可變,而不是說常量池中array本身的數據元素不可變!
而String對象的改變實際上是通過內存地址的 “斷開-連接” 變化來完成的,這個過程中原字符串中的內容并沒有任何的改變。String s = "yiyige"; 和 s = "yyg"; 實質上是開辟了2個內存空間,s 只是由原來指向 "yiyige" 變?yōu)橹赶?"yyg" 而已,而其原來的字符串內容,是沒有改變的,如下圖所示。
因此,我們在以后的開發(fā)中,如果要經常修改字符串的內容,請盡量少用String,因為字符串的指向“斷開-連接”會大大降低性能,建議使用:StringBuilder、StringBuffer。
那么String一定不可變嗎?有沒有辦法讓String真的可變呢?我們繼續(xù)往下學習!
三. String真的不可變嗎?
1. 實驗案例
我在前面的章節(jié)中給大家說,String的不可變,其實指的是String類中value屬性在棧中的引用地址不可變,而不是說常量池中array本身的數據元素不可變!也就是說String字符串的內容其實是可變的!那怎么實現(xiàn)呢?利用反射就可以實現(xiàn),我們通過一個案例來證明一下。
try { String str = "yyg"; System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str)); Class stringClass = str.getClass(); //獲取String類中的value屬性 Field field = stringClass.getDeclaredField("value"); //設置私有成員的可訪問性,進行暴力反射 field.setAccessible(true); //獲取value數組中的內容 char[] value = (char[]) field.get(str); System.out.println("value=" + Arrays.toString(value)); value[1] = 'z'; System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str)); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); }
2. 結果剖析
上面案例的執(zhí)行結果如下圖所示:
我們可以看到,String字符串的字符數組可以通過反射進行修改,導致字符串的“內容”真的發(fā)生了變化! 并且我們又利用底層的java.lang.System#identityHashCode()方法(不管是否重寫了hashCode方法)獲取了對象的唯一哈希值,該方法獲取的hash值與hashCode()方法是一樣的。我們可以看到兩個字符串的唯一性hash值是一樣的,證明字符串引用地址沒有發(fā)生改變!所以在這里,我們并不是像之前那樣創(chuàng)建了一個新的String字符串,而是真的改變了String的內容。這個代碼案例進一步說明,String類的不可變指的是中value屬性在棧中的引用地址不可變,而不是說常量池中array本身的數據元素不可變!也就是說String字符串的內容其實是可變的!
四. 結語
String作為Java中使用最為廣泛的一個類,之所以設計為不可變,主要是出于效率與安全性方面考慮。這種設計有優(yōu)點,也有缺點。
1. 不可變性的優(yōu)點
只有當字符串是不可變的,字符串池才有可能實現(xiàn)。 字符串池的實現(xiàn)可以在運行時節(jié)約很多heap空間,因為不同的字符串引用都可以指向池中的同一個字符串。但如果字符串是可變的,如果一個引用變量改變了字符串的值,那么其它指向這個值的變量內容也會跟著一起改變。
如果字符串是可變的,那么可能會引起很嚴重的安全問題。 譬如,數據庫的用戶名、密碼都是以字符串的形式傳入數據庫,以獲得數據庫的連接;或者在socket編程中,主機名和端口都是以字符串的形式傳入。因為字符串是不可變的,所以它的值是不可改變的,否則黑客們可以鉆到空子,改變字符串指向的對象值,造成安全漏洞。
因為字符串是不可變的, 在物理上是絕對的線程安全 ,所以同一個字符串實例可以被多個線程共享。 由于不可變對象不可能被修改,因此能夠在多線程中被任意自由訪問而不導致線程安全問題,不需要多余的同步操作。即在并發(fā)場景下,多個線程同時讀一個資源,并不會引發(fā)競態(tài)條件,只有對資源進行讀寫才有危險。不可變對象不能被寫,所以線程安全。
類加載器要用到字符串,不可變性也提供了安全性,以便正確的類可以被加載。 譬如你想加載java.sql.Connection類,而這個值被改成了myhacked.Connection,那么會對你的數據庫造成不可知的破壞。
因為字符串是不可變的,所以在字符串對象創(chuàng)建的時候hashCode()就被執(zhí)行并把執(zhí)行結果緩存了,不需要重新計算。這就使得字符串很適合作為Map中的鍵,所以字符串的處理速度要快過其它的鍵對象,這就是HashMap中的鍵往往都使用字符串的原因,當我們需要頻繁讀取訪問任意鍵值對時,能夠節(jié)省很多的CPU計算開銷。
Sting的不可變性會提高執(zhí)行性能和效率,基于Sting不可變,我們就可以用緩存池將String對象緩存起來,同時把一個String對象的地址賦值給多個String引用,這樣可以安全保證多個變量共享同一個對象。因此,構造一萬個string s = "xyz",實際上得到都是同一個字符串對象,避免了很多不必要的空間開銷。
2. 不可變性的缺點
- 喪失了部分靈活性。我們平時使用的大部分都是可變對象,比如內容變化時,只需要利用setValue()更新一下就可以了,不需要重新創(chuàng)建一個對象,但是String很難做到這一點。當然,我們完全可以使用StringBuilder來彌補這個缺點。
- 脆弱的不可變性,String其實可以利用JNI或反射來改變其不可變性。
以上就是Java字符串原理分析之String是否可變的詳細內容,更多關于Java String原理的資料請關注腳本之家其它相關文章!
相關文章
Spring Boot集成Spring Cloud Security進行安全增強的方法
Spring Cloud Security是Spring Security的擴展,它提供了對Spring Cloud體系中的服務認證和授權的支持,包括OAuth2、JWT等,這篇文章主要介紹了Spring Boot集成Spring Cloud Security進行安全增強,需要的朋友可以參考下2024-11-11