String系列
很多時候,我們都會認爲我們十分的瞭解String,久而久之,在使用過程中,就會踩一些坑。
從String的+實現原理引發的一系列問題
先來看一道題,你認爲結果如何?
String a = "helloWord1";
final String b = "helloWord";
String F = "helloWord";
String c = b + 1;
String e = F + 1;
System.out.println((a == c));
System.out.println((a == e));
// 獲取內存地址
// System.out.println(System.identityHashCode(a));
// System.out.println(System.identityHashCode(c));
// System.out.println(System.identityHashCode(e));
我先不說結果,對的人自然對,不對的人自然不對,不過不要緊,你花一點兒時間聽我嘮叨完。
習慣了使用String str = str1 + str2
,卻從未想過以前經常準備過的面試題:String
類型是不可變。對呀,明明不可變,那這裏爲啥看起來好像可以變呢,是不是我們在進行一次str = str1
,就該可以改變了呢,答案當然是否定的。
這裏講下這個+
的原理:實際上底層(編譯器做的事)是使用StringBuilder
(JDK1.5
之前是StringBuffer
)的append
來完成字符串的拼接,對於每一個 str1
,則調用其String.valueOf(str1)
獲取其值,最後的執行效果如:
String str = (new StringBuilder(String.valueOf(str1))).append(String.valueOf(str2)).toString()
看到這裏是不是對String
類型是不可變的加深了理解了呢,因爲每次都是new StringBuilder()
,所以這裏看似可變,其實每次都是new
出的新對象,底層實際上是更改了棧內變量名指向的地址,使之指向堆中新的對象。(當然,String爲什麼不可變,原理不僅僅於此,源碼更能解釋String如何實現了不可變,感興趣的,繼續往下翻)
明白了+
號實現原理,是不是可以舉一反三,請看以下代碼:
String str = null;
str += "a";
System.out.println(str);
你認爲輸出是什麼呢?
結果是:nulla
爲什麼呢,根據上述原理分析,在StringBuilder
進行append
之前,會對對象進行String.valueof()
,那麼我們來翻下源碼:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
這下應該明白了,所以平時在開發中針對字符串的處理,一定要三思而後行。
String是不可變的
什麼是不可變對象
如果一個對象它被創建後,狀態不能改變,則這個對象被認爲是不可變的。
爲什麼不可變
String
的底層實際上一個char數組,相信好奇心重的盆友都翻開過源碼,那麼我把他扒出來的目的只有一個,大家請看char數組是不是多了個修飾:final。
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
被final修飾的變量在其初始化完成後,不可變。
補充一句,這裏的不可變是指引用變量所引用的地址不可變, 即一直引用同一個對象,但這個對象完全可以發生改變 。 例如某個指向數組的final引用,它必須從此至終指向初始化時指向的數組,但是這個數組的內容完全可以改變。
既然存在了final,那麼value引用的地址,肯定無法變了。再看看內容,貌似沒有任何一個方法,允許我們直接修改value的內容。到此已經可以證明:String不可變。
但是,String真的不可變嗎?
不死心呀,程序員可是能上天入地的,這點小事我就要從了嗎?客官彆着急,還真有法子打破String不可變的真理——反射,這個強大的功能,讓我們可以強行修改value的值,上例子:
String str = "Hello World";
System.out.println("修改前的str:" + str);
System.out.println("修改前的str的內存地址" + System.identityHashCode(str));
// 獲取String類中的value字段
Field valueField = String.class.getDeclaredField("value");
// 改變value屬性的訪問權限
valueField.setAccessible(true);
// 獲取str對象上value屬性的值
char[] value = (char[]) valueField.get(str);
// 改變value所引用的數組中的字符
value[3] = '?';
System.out.println("修改後的str:" + str);
System.out.println("修改前的str的內存地址" + System.identityHashCode(str));
執行結果:
修改前的str:Hello World
修改前的str的內存地址1784662007
修改後的str:Hel?o World
修改前的str的內存地址1784662007
可以看到了,內存地址未變,但是內容變掉了。
總結:String類型是不可變的,但是在我們使用了反射之後,往往是可以打破這些原則。