什么是不可變對(duì)象?
如果一個(gè)對(duì)象,在它創(chuàng)建完成之后,不能再改變它的狀態(tài),那么這個(gè)對(duì)象就是不可變的。不能改變狀態(tài)的意思是,不能改變對(duì)象內(nèi)的成員變量,包括基本數(shù)據(jù)類(lèi)型的值不能改變,引用類(lèi)型的變量不能指向其他的對(duì)象,引用類(lèi)型指向的對(duì)象的狀態(tài)也不能改變
定義一個(gè)字符串
String s = "abcd";

s 中保存了 String 對(duì)象的引用。
使用變量來(lái)賦值變量
String s2 = s;

s2 保存了相同的引用值,因?yàn)樗麄兇硗粋€(gè)對(duì)象。
字符串連接
s = s.concat("ef");

s 中保存的是一個(gè)重新創(chuàng)建出來(lái)的 String 對(duì)象的引用。
String 總結(jié)
一旦一個(gè) String 對(duì)象在內(nèi)存(堆)中被創(chuàng)建出來(lái),它就無(wú)法被修改(因?yàn)?String 類(lèi)的所有成員變量都是 private,并且沒(méi)有提供 public 的 set 方法來(lái)修改這些值。此外成員變量都是 final 的,這就意味著一旦初始化就無(wú)法修改)。特別要注意的是,String 類(lèi)的所有方法都沒(méi)有改變字符串本身的值,都是返回了一個(gè)新的對(duì)象
如果需要一個(gè)可修改的字符串,應(yīng)該使用 StringBuffer 或者 StringBuilder。否則會(huì)有大量時(shí)間浪費(fèi)在垃圾回收上,因?yàn)槊看卧噲D修改都有新的 String 對(duì)象被創(chuàng)建出來(lái)
String 類(lèi)不可變性的好處
只有當(dāng)字符串是不可變的,字符串池才有可能實(shí)現(xiàn)。字符串池的實(shí)現(xiàn)可以在運(yùn)行時(shí)節(jié)約很多 heap 空間,因?yàn)椴煌淖址兞慷贾赶虺刂械耐粋€(gè)字符串。但如果字符串是可變的,那么將不能實(shí)現(xiàn) String interning(指對(duì)不同的字符串僅僅只保存一個(gè),即不會(huì)保存多個(gè)相同的字符串),因?yàn)檫@樣的話(huà),如果變量改變了它的值,那么其它指向這個(gè)值的變量的值也會(huì)一起改變
如果字符串是可變的,那么會(huì)引起很?chē)?yán)重的安全問(wèn)題。譬如,數(shù)據(jù)庫(kù)的用戶(hù)名、密碼都是以字符串的形式傳入來(lái)獲得數(shù)據(jù)庫(kù)的連接,或者在 socket 編程中,主機(jī)名和端口都是以字符串的形式傳入。因?yàn)樽址遣豢勺兊?,所以它的值是不可改變的,否則黑客們可以鉆到空子,改變字符串指向的對(duì)象的值,造成安全漏洞
因?yàn)樽址遣豢勺兊?,所以是多線(xiàn)程安全的,同一個(gè)字符串實(shí)例可以被多個(gè)線(xiàn)程共享。這樣便不用因?yàn)榫€(xiàn)程安全問(wèn)題而使用同步。字符串自己便是線(xiàn)程安全的
類(lèi)加載器要用到字符串,不可變性提供了安全性,以便正確的類(lèi)被加載。譬如你想加載 java.sql.Connection 類(lèi),而這個(gè)值被改成了 myhacked.Connection,那么會(huì)對(duì)你的數(shù)據(jù)庫(kù)造成不可知的破壞
因?yàn)樽址遣豢勺兊?,所以在它?chuàng)建的時(shí)候 hashcode 就被緩存了,不需要重新計(jì)算。這就使得字符串很適合作為 Map 中的鍵,字符串的處理速度要快過(guò)其它的鍵對(duì)象。這就是為什么 HashMap 中的鍵往往都使用字符串
String 對(duì)象真的不可變嗎
JDK 6 String 源碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
JDK 7 String 源碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
String 的成員變量是 private final 的,也就是初始化之后不可改變。但是在這幾個(gè)成員中, value 比較特殊,因?yàn)樗且粋€(gè)引用變量,而不是真正的對(duì)象。value 是 final 修飾的,也就是說(shuō) final 不能再指向其他數(shù)組對(duì)象,那么能改變 value 指向的數(shù)組嗎? 比如將數(shù)組中的某個(gè)位置上的字符變?yōu)橄聞澗€(xiàn) “_”。至少在我們自己寫(xiě)的普通代碼中不能夠做到,因?yàn)槲覀兏静荒軌蛟L(fǎng)問(wèn)到這個(gè) value 引用,更不能通過(guò)這個(gè)引用去修改數(shù)組
那么用什么方式可以訪(fǎng)問(wèn)私有成員呢? 沒(méi)錯(cuò),用反射,可以得到 String 對(duì)象中的 value 屬性, 進(jìn)而改變通過(guò)獲得的 value 引用改變數(shù)組的結(jié)構(gòu)
public void testReflection() throws Exception {
//創(chuàng)建字符串"Hello World", 并賦給引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//獲取String類(lèi)中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改變value屬性的訪(fǎng)問(wèn)權(quán)限
valueFieldOfString.setAccessible(true);
//獲取s對(duì)象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改變value所引用的數(shù)組中的第5個(gè)字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
結(jié)果:
s = Hello World
s = Hello_World
在這個(gè)過(guò)程中,s 始終引用的同一個(gè) String 對(duì)象,但是在反射前后,這個(gè) String 對(duì)象發(fā)生了變化,也就是說(shuō),通過(guò)反射是可以修改所謂的“不可變”對(duì)象的。但是一般我們不這么做。這個(gè)反射的實(shí)例還可以說(shuō)明一個(gè)問(wèn)題:如果一個(gè)對(duì)象,它組合的其他對(duì)象的狀態(tài)是可以改變的,那么這個(gè)對(duì)象很可能不是不可變對(duì)象。例如一個(gè) Car 對(duì)象,它組合了一個(gè) Wheel 對(duì)象,雖然這個(gè) Wheel 對(duì)象聲明成了 private final 的,但是這個(gè) Wheel 對(duì)象內(nèi)部的狀態(tài)可以改變, 那么就不能很好的保證 Car 對(duì)象不可變