一、前言
關于String的介紹,可以先參考我的另外一篇文章【JAVA】String源碼淺談
本篇文章,主要來探讨String的不可變性
二、到底什麼是不可變
可以這樣了解,一個對象在建立完成後,不能去改變它的狀态,不能改變它的成員變量。如果變量包含基本資料類型,那麼這個基本資料類型的值不能改變;如果包含引用類型,那麼這個引用類型的變量不能指向别的對象,而且該引用類型指向的變量的狀态也不能改變。
這裡引用一下比較官方的解釋,Effective Java 中第 15 條 使可變性最小化 中對 不可變類 的解釋:
不可變類隻是其執行個體不能被修改的類。每個執行個體中包含的所有資訊都必須在建立該執行個體的時候就提供,并且在對象的整個生命周期内固定不變。為了使類不可變,要遵循下面五條規則:
- 不要提供任何會修改對象狀态的方法。
- 保證類不會被擴充。 一般的做法是讓這個類稱為 final 的,防止子類化,破壞該類的不可變行為。
- 使所有的域都是 final 的。
- 使所有的域都成為私有的。 防止用戶端獲得通路被域引用的可變對象的權限,并防止用戶端直接修改這些對象。
- 確定對于任何可變性元件的互斥通路。 如果類具有指向可變對象的域,則必須確定該類的用戶端無法獲得指向這些對象的引用。
三、新手的疑惑
public class Main {
public static void main(String[] args) {
String a="abcd";
a="efg";
System.out.println(a);
}
}
這段代碼可以輸出efg,a确實String類型的啊,那現在a被改變了,說明String是可變的啊。
我們可以輸出a在改變前後所指向的對象位址
可以看得出,a在改變前後,所指向的對象位址不同,也就是說,a僅僅是指向了不同的對象,但對象abcd本身的狀态并未改變,"abcd"還是那個"abcd",是以說,String是不可變的。
四、不可變類的好處
【1】安全可靠
String在java中用處廣泛,例如資料庫的連接配接字元串、網絡請求的url、io操作的檔案名與類加載機制傳遞的類名字元串等。如果String是可變的,将會十分危險。
【2】簡單高效
1、String緩存其哈希碼
多次調用String對象的hashCode方法,最多隻會在第一次調用時,計算它的哈希碼,此後的調用,将直接傳回緩存的哈希碼。這使得,它作為HashMap的健時,計算索引位置非常的高效。
這提醒我們,不要用可變對象做HashMap或HashSet的鍵,否則特别容易引起匪夷所思的問題。
2、String常量池
關于String的常量池有關的内容,可以參考我的另外一篇文章【JAVA】字元串的建立與存儲機制
有了String常量池,那麼在大量使用字元串的情況下,可以節省記憶體空間,提高效率。但之是以能實作這個特性,String的不可變性是最基本的一個必要條件。要是記憶體裡字元串内容能改來改去,這麼做就完全沒有意義了。
【3】線程安全
不可變對象本質上就是線程安全的,它們不要求同步,不可變對象可以被自由地共享。
五、String真的無法改變嗎
仔細觀察String的成員變量,可以發現,value數組雖然被final修飾,但是我們依然可以改變數組中元素的内容,隻是不能更改引用的指向罷了。
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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
}
現在嘗試一下,改變某個元素内容。還是老方法,利用反射機制。
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) {
String s = "abcd";
System.out.println("改變前的位址:" + System.identityHashCode(s));
System.out.println("改變前的哈希:"+s.hashCode());
try {
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] array = (char[]) field.get(s);
array[0] = 'b';
System.out.println(s);
System.out.println("改變後的位址:" + System.identityHashCode(s));
System.out.println("改變後的哈希:"+s.hashCode());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
輸出: