其實在Java中,String類被final修飾,主要是為了保證字串的不可變性,進而保證了它的安全性,那么final到底是怎么保證字串安全性的呢?接下來就讓我們一起來看看吧,
一. final的作用
1. final關鍵詞修飾的類不可以被其他類繼承,但是該類本身可以繼承其他類,通俗地說就是這個類可以有父類,但不能有子類,
final class MyTestClass1 {
// ...
}
2. final關鍵詞修飾的方法不可以被覆寫重寫,但可以被繼承使用,
class MyTestClass2 {
final void myMethod() {
// ...
}
}
3. final關鍵詞修飾的基本資料型別被稱為常量,只能被賦值一次,
class MyTestClass3 {
final int number = 100;
}
4. final關鍵詞修飾的參考資料型別變數,其值為地址值,該地址值不能改變,但該地址對應的資料物件可以被改變(其實這一點就和我們今天要說的內容有關了,在后面我會結合案例跟大家重點解釋,大家一定要打起精神仔細學習哦),
5. final關鍵詞修飾的成員變數,需要在創建物件前就賦值,否則會報錯(即需要在定義時直接賦值),
綜上所述,我們可以知道,final在Java中是一個非常有用的關鍵字,主要可以提高我們代碼的穩定性和可讀性,當然,我們今天要講解的重點是被final修飾的String類,所以接下來我們還是把目光轉回到String身上來,看看String都有哪些特性吧!
二. 被final修飾的String類
為了讓大家更好地理解String的不可變性,首先我要給各位簡要地講一下String的原始碼設計,從下面的這段原始碼中,我們可以搞清楚很多底層的設計思路,接下來就請大家跟著我一起來看看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小節對final特點的介紹;
● Serializable:用于序列化;
● Comparable<String>:默認的比較器;
● CharSequence: 提供對字符序列進行統一、只讀的操作,
從這段原始碼及其注釋中,我們可以得到下面這些結論:
● String類用final關鍵字修飾,說明String不可被繼承;
● String字串是常量,字串的值一旦被創建,就不能被改變;
● String字串緩沖區支持可變字串;
● String物件是不可變的,它們是可以被共享的,
三. String的不可變性
在學習了上面的這些核心原始碼之后,接下來,我們可以通過一個案例來實踐驗證一番,看看String字串的內容到底能不能改變,這里有個代碼案例,如下圖所示:

在上述的案例結果中,大家可以看出,s的內容竟然發生了改變?!但我們不是一直說String是不可變的嗎?這是咋回事?大家先別急,我們繼續往下看,
要想弄明白這個問題,我們首先得知道一個知識點:參考和值的區別!
在上面的代碼中,我們先是創建了一個 "yiyige" 為內容的字串參考s,如下圖:

s其實先是指向了value物件,而value物件又指向了存盤 "y,i,y,i,g,e" 字符的字符陣列,但因為value被final修飾,所以value的值不可被更改,因此,上面代碼中改變的其實是s的參考指向,而不是改變了String物件的值!
換句話說,上面實體中s的值,其實只是value的參考地址,并不是String的內容本身,當我們執行 s = "yyg" 陳述句時,Java會創建一個新的字面量物件 "yyg",而原來的 "yiyige" 字面量物件其實依然存在于記憶體的intern快取池中,
在這里,String物件的改變,實際上是通過記憶體地址的“斷開-連接”變化來完成的,在這個程序中,原字串中的內容并沒有發生任何的改變,String s = "yiyige" 和 s = "yyg"這兩行代碼,實質上是開辟了2個記憶體空間,s只是由原來指向 "yiyige" 變為指向 "yyg" 而已,而其原來的字串內容,是沒有發生改變的,如下圖所示,

因此,我們在以后的開發中,如果要經常修改字串的內容,請盡量少用String!因為如果字串的指向經常的“斷開-連接”,就會大大降低性能,我建議大家使用StringBuilder 或 StringBuffer 進行替換,
我們繼續把上面的代碼深入地分析一下,在Java中,因為陣列也是物件, 所以value中存盤的也只是一個參考,它指向一個真正的陣列物件,在執行了String s = “yiyige”; 這句代碼之后,真正的記憶體布局應該是下圖這樣的:

因為value是String封裝的字符陣列,value中所有的字符都屬于String這個物件,而由于value是private的,沒有提供setValue等公共方法來修改這個value值,所以我們在String類的外部是無法修改value值的,也就是說字串一旦初始化就不能再被修改,
此外,value變數是final修飾的,也就是說在String類內部,一旦這個值初始化了,value這個變數所參考的地址就不會改變了,即一直參考同一個物件,正是基于這一層,我們才說String物件是不可變的物件,
所以String的不可變,其實是指value在堆疊中的參考地址不可變,而不是說常量池中value字符陣列里的資料元素不可變,也就是說,value所參考的陣列物件里的內容,其實是可以發生改變的,
那么我們又如何改變它呢?這就要通過反射來消除String類物件的不可變性啦!
四. String真的不可變嗎?
在上述內容中,我們重點給大家解釋了String字串的可變性,現在大家應該已經知道了,String字串的內容其實是可變的,不可改變的只是String字串的物件地址,那么我們到底該怎么讓String字串的內容發生改變呢?在上述我們給大家提到了反射,接下來我們就來看看如何通過反射改變String字串的內容吧,代碼案例如下所示:
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 = https://www.cnblogs.com/qian-fen/p/(char[]) field.get(str);
System.out.println("value="https://www.cnblogs.com/qian-fen/p/+ Arrays.toString(value));
value[1] ='z';
System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
執行結果如下圖所示:

從上面的結果中我們可以看到,String字串的字符陣列,通過反射進行修改后,字串的“內容”真的發生了變化!
并且我們又利用底層的java.lang.System#identityHashCode()方法(不管是否重寫了hashCode方法),來獲取到了該字串物件的唯一哈希值,該方法獲取的hash值與hashCode()方法是一樣的,
從結果中,我們可以看到兩個字串的唯一hash值是一樣的,這就證明字串的參考地址沒有發生改變,
所以這就說明,我們并不是像之前那樣創建了一個新的String字串,而是真的改變了原有String的內容,
這個代碼案例進一步證明了我們上面的結論:String字串的不可變,指的其實是value物件在堆疊中的參考地址不可變,而不是說常量池中value里的資料元素不可變!簡單地說,就是String字串的內容其實是可以改變的,不能改表的是它的物件地址而已,
所以這也就是我們上述所說的final的作用之一:final關鍵詞修飾的參考資料型別的變數,其值為地址值,地址值不能改變,但是地址內的資料物件可以被改變!
五. 總結
至此,我們就把今天的面試題分析完了,現在你明白了嗎?最后我再來給大家總結一下今天的重點內容吧:
1. 為什么要用final修飾java中的String類呢?
核心:因為它確保了字串的安全性和可靠性,
2. java中的String真的不可變嗎?
核心:String字串的內容其實是可變的,但要通過特殊手段進行實作,不可改變的是String字串物件的地址,
3. 如何消除String類物件的不可變性?
核心:利用反射來消除String類物件的不可變性,
4. 如果想要保證String的不可變要注意哪些?
● 首先,將 String 類宣告為 final型別,這意味著String類是不可被繼承的,防止程式員通過繼承重寫String類的某些方法,使得String類出現“可變的”的情況;
● 然后,重要的字符陣列value屬性,要被private 和 final修飾,它是String的底層陣列,用于存貯字串內容,又因為陣列是參考型別,所以只能限制參考不被改變,也就是說陣列元素的值是可以改變的,這在上面的案例中已經證明過了;
● 接著,所有修改的方法都回傳新的字串物件,保證修改時不會改變原始物件的參考;
● 最后,不同的字串物件都可以指向快取池中的同一個字串字面量,
當然,“Java中的String類使用final修飾”這個概念非常重要,因為它確保了字串的安全性和可靠性,但是我們也要清楚不可改變的只是它的地址,而不是它的內容,它的內容是可以利用反射來改變的!只不過在一般的描述中,大家都會說String內容不可改變,畢竟很多時候是不允許利用反射這種特殊的功能去進行這樣的操作的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/547607.html
標籤:Java
