為了加深對Java語言的理解,加深對Java各種特性的理解與掌握,平常會自己歸納一些專題的分析和總結(jié)?;谧约旱睦斫?,感覺哪些部分適合在一起進行總結(jié),就歸納為一個專題了??赡芤粋€專題里面的東西也不屬于一個類別,或者也比較雜亂,請見諒。
本文是前段時間對于不可變對象的學習,然后聯(lián)系到了一個非常重要的不可變對象String,所以在此對這兩者進行一個整理與總結(jié)。
文中部分論證方式和結(jié)論是在一些博客上學到,借鑒過來的,如有侵權(quán),請聯(lián)系刪除。
1. 什么是不可變對象、不可變類
不可變對象的狀態(tài)在構(gòu)造后不能被修改,任何修改都應(yīng)產(chǎn)生新的不可變對象。
不可變類的所有屬性都應(yīng)該是final。
不可變對象應(yīng)該是final的,以限制子類來修改父類的不變性。
不可變對象必須正確構(gòu)造,即對象引用在構(gòu)造過程中不能泄漏。
不可變對象的類即為不可變類。如String、基本類型的包裝類、BigInteger和BigDecimal等。
2. 不可變對象的優(yōu)缺點
優(yōu)點:
- 構(gòu)造、測試、使用簡單
- 不可變對象是線程安全的,在線程之間可以相互共享,不需要利用特殊機制保證同步問題;因為對象的值無法改變,所以不需要鎖機制來保持內(nèi)存一致性。
- 不可變對象可以被重復使用,可以將它們緩存起,就像字符串字面量和整型數(shù)字一樣??梢允褂渺o態(tài)工廠方法來提供類似于valueOf()這樣的方法,從緩存中返回一個已經(jīng)存在的Immutable對象。
public class CacheImmutale {
private final String name;
private static CacheImmutale[] cache = new CacheImmutale[10];
private static int pos = 0;
public CacheImmutale(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public static CacheImmutale valueOf(String name) {
// 遍歷已緩存的對象
for (int i = 0; i < pos; i++) {
// 如果已有相同實例,直接返回該緩存的實例
if (cache[i] != null && cache[i].getName().equals(name)) {
return cache[i];
}
}
// 如果緩沖池已滿
if (pos == 10) {
// 把緩存的第一個對象覆蓋
cache[0] = new CacheImmutale(name);
pos = 1;
return cache[0];
} else {
// 把新創(chuàng)建的對象緩存起來,pos加1
cache[pos++] = new CacheImmutale(name);
return cache[pos - 1];
}
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CacheImmutale) {
CacheImmutale ci = (CacheImmutale) obj;
if (name.equals(ci.getName())) {
return true;
}
}
return false;
}
public static void main(String[] args) {
CacheImmutale c1 = CacheImmutale.valueOf("hello");
CacheImmutale c2 = CacheImmutale.valueOf("hello");
System.out.println(c1 == c2);// 輸出結(jié)果為true
}
}
缺點:
- 創(chuàng)建對象的開銷,因為每一步操作都會產(chǎn)生一個新的對象,占用大量的內(nèi)存;對于他們很多情況都是使用后就扔掉,制造很多垃圾。
3. 怎樣創(chuàng)建不可變類
- 將類聲明為final,確保類不能被繼承。(因為繼承會破壞類的不可變特性,繼承類覆蓋父類的方法并且繼承類可以改變成員變量值,那么就不能保證父類不可變)
- 將所有的成員聲明為private和final的,這樣就不允許直接訪問這些成員。
- 如果成員屬性為可變對象屬性,不能共享對可變對象的引用,不要存儲傳給構(gòu)造器的外部可變對象的引用。(因為可變對象的成員變量的引用和外部可變對象的引用指向同一塊內(nèi)存地址,這樣可以在不可變對象之外修改可變屬性的值)。
//存儲傳給構(gòu)造器的外部可變對象的引用
public final class ImmutableTest {
private final int[] array;
public ImmutableTest(int[] array) {
this.array = array; //error
}
public int[] getArray() {
return array;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4};
ImmutableTest immutableTest = new ImmutableTest(arr);
System.out.println(Arrays.toString(immutableTest.getArray()));
//在外部改變可變對象的值
arr[2] = 5;
System.out.println(Arrays.toString(immutableTest.getArray()));
}
}
//Output:
[1, 2, 3, 4]
[1, 2, 5, 4]
//使用深度拷貝的方法來復制一個對象并傳入副本的引用來確保類的不可變
public final class ImmutableTest {
private final int[] array;
public ImmutableTest(int[] array) {
this.array = array.clone(); //clone
}
public int[] getArray() {
return array;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4};
ImmutableTest immutableTest = new ImmutableTest(arr);
System.out.println(Arrays.toString(immutableTest.getArray()));
arr[2] = 5;
System.out.println(Arrays.toString(immutableTest.getArray()));
}
}
//Output:
[1, 2, 3, 4]
[1, 2, 3, 4]
- 通過構(gòu)造器初始化所有成員,進行深拷貝(deep copy)。
- 在getter方法中,不要直接返回對象本身,而是克隆對象,并返回對象的拷貝。
//構(gòu)造器淺拷貝
public class ImmutableTest2 {
private final int id;
private final String name;
private final HashMap testMap;
public int getId() {
return id;
}
public String getName() {
return name;
}
//返回實際引用
public HashMap getTestMap() {
return testMap;
}
//淺拷貝構(gòu)造器
public ImmutableTest2(int id, String name, HashMap testMap) {
this.id = id;
this.name = name;
this.testMap = testMap;
}
public static void main(String[] args) {
int i = 1;
String s = "a";
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("1", "a");
hashMap.put("2", "b");
ImmutableTest2 immutableTest2 = new ImmutableTest2(i, s, hashMap);
System.out.println(s == immutableTest2.getName());
System.out.println(hashMap == immutableTest2.getTestMap());
System.out.println("1,id=" + immutableTest2.getId());
System.out.println("1,name=" + immutableTest2.getName());
System.out.println("1,hashmap=" + immutableTest2.getTestMap());
i = 2;
s = "b";
hashMap.put("3","c");
System.out.println("2,id=" + immutableTest2.getId());
System.out.println("2,name=" + immutableTest2.getName());
System.out.println("2,hashmap=" + immutableTest2.getTestMap());
HashMap<String,String> hashMap1 = immutableTest2.getTestMap();
hashMap1.put("4","d");
System.out.println("3,hashmap="+immutableTest2.getTestMap());
}
}
//Output:
true
true
1,id=1
1,name=a
1,hashmap={1=a, 2=b}
2,id=1
2,name=a
2,hashmap={1=a, 2=b, 3=c}
3,hashmap={1=a, 2=b, 3=c, 4=d}
//可以看出,hashmap的值被更改了,這是因為構(gòu)造器實現(xiàn)的是淺拷貝,而且在get方法中返回的是原來的引用。
//構(gòu)造器深拷貝
public class ImmutableTest2 {
private final int id;
private final String name;
private final HashMap testMap;
public int getId() {
return id;
}
public String getName() {
return name;
}
//拷貝
public HashMap getTestMap() {
// return testMap;
return (HashMap) testMap.clone();
}
//淺拷貝構(gòu)造器
// public ImmutableTest2(int id, String name, HashMap testMap) {
// this.id = id;
// this.name = name;
// this.testMap = testMap;
// }
//深拷貝構(gòu)造器
public ImmutableTest2(int id, String name, HashMap testMap) {
this.id = id;
this.name = name;
HashMap<String, String> tempMap = new HashMap<>();
String key;
Iterator iterator = testMap.keySet().iterator();
while (iterator.hasNext()) {
key = iterator.next().toString();
tempMap.put(key, testMap.get(key).toString());
}
this.testMap = tempMap;
}
public static void main(String[] args) {
int i = 1;
String s = "a";
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("1", "a");
hashMap.put("2", "b");
ImmutableTest2 immutableTest2 = new ImmutableTest2(i, s, hashMap);
System.out.println(s == immutableTest2.getName());
System.out.println(hashMap == immutableTest2.getTestMap());
System.out.println("1,id=" + immutableTest2.getId());
System.out.println("1,name=" + immutableTest2.getName());
System.out.println("1,hashmap=" + immutableTest2.getTestMap());
i = 2;
s = "b";
hashMap.put("3", "c");
System.out.println("2,id=" + immutableTest2.getId());
System.out.println("2,name=" + immutableTest2.getName());
System.out.println("2,hashmap=" + immutableTest2.getTestMap());
HashMap<String, String> hashMap1 = immutableTest2.getTestMap();
hashMap1.put("4", "d");
System.out.println("3,hashmap=" + immutableTest2.getTestMap());
}
}
//Output:
true
false
1,id=1
1,name=a
1,hashmap={1=a, 2=b}
2,id=1
2,name=a
2,hashmap={1=a, 2=b}
3,hashmap={1=a, 2=b}
//這樣構(gòu)造,hashmap的值不會被更改
4. String的不可變是怎么實現(xiàn)的
這里對于初學者,可能會對于String是不可變對象存在疑惑,比如:
public static void main(String[] args){
String str = "123";
System.out.println("str = " + str);
str = "abc";
System.out.println("str = " + str);
}
//Output:
str = 123
str = abc
從打印結(jié)果來看,這里的s的值的確是變化了啊,怎么說是不可變的呢?
這里就是對象的引用問題,對象是存在于堆區(qū),而str只是一個String對象的引用,存放了指向這個對象的地址,并不是這個對象本身,然后通過這個引用可以訪問這個對象。
所以這里當str="123";執(zhí)行之后,str就指向了"123"這個對象,然后str="abc";執(zhí)行之后,str又指向了新創(chuàng)建的"abc"對象,原來的"123"對象在堆中還是存在,并且沒有改變。
image.png
那么再來看String的不可變是怎么實現(xiàn)的:
String.java:
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
JDK1.8下,主要成員變量就2個,在這里hash成員變量是String對象的哈希值的緩存,與此類題關(guān)系不大。所以與String是不可變對象有關(guān)的是value數(shù)組,java里面,數(shù)組也是對象,所以這里的value也是一個引用,指向了真正的數(shù)組對象。
image.png
在這里value變量是private的,并且沒有提供set方法和其他公共方法,所以在String外部不能修改這個值;然后value變量也是final的,所以在String內(nèi)部,一旦初始化后也就不能修改,所以可以認為String是不可變對象。
在這里可能還有一個疑問,在String類中,存在一些方法,調(diào)用它們之后可以得到改變后的值,substring、replace等。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
這里看源碼可以知道,最后是在內(nèi)部新創(chuàng)建了一個String對象,然后把這個新的對象的引用返回出去重新賦值給調(diào)用者。
5. String為什么設(shè)置成不可變對象
- 字符串常量池的需要,這里應(yīng)該說字符串常量池的設(shè)計是利用了String是不可變對象來進行的一種優(yōu)化的手段;(這里講到字符串常量池,可參考我的另一篇總結(jié)專題整理之—String的字符串常量池)
- 允許String對象緩存hashCode:在String類中有
/** Cache the hash code for the string */
private int hash; // Default to 0
字符串的不變性保證了hash碼的唯一,因此可以放心的使用緩存,這也是一種性能優(yōu)化的手段。因為String對象的hash碼被頻繁的使用,比如在hashmap等容器中;
- 安全性:
傳遞安全:因為java對象參數(shù)傳的引用,所以可變的StringBuffer參數(shù)就被改變了,可以看到變量sb在appendSb之后,就變成了"aaabbb",如果String也是可變對象,就會出現(xiàn)這個問題。
Class Test{
//不可變的String
public static String appendStr(String s){
s+="bbb";
return s;
}
//可變的StringBuilder
public static StringBuilder appendSb(StringBuilder sb){
return sb.append("bbb");
}
public static void main(String[] args){
String s = new String("aaa");
String ns = Test.appendStr(s);
System.out.println("String aaa>>>"+s.toString());
//StringBuilder做參數(shù)
StringBuilder sb = new StringBuilder("aaa");
StringBuilder nsb = Test.appendSb(sb);
System.out.println("StringBuilder aaa >>>"+sb.toString());
}
}
//Output:
String aaa>>>aaa
StringBuilder aaa >>>aaabbb
線程安全:在并發(fā)場景下,多個線程同時讀一個資源,不會引發(fā)競爭條件;只有對資源進行寫操作時才會競爭,不可變對象不能被寫,所以線程安全。
String被許多的Java類(庫)用來當做參數(shù),例如 網(wǎng)絡(luò)連接地址URL,文件路徑path,還有反射機制所需要的String參數(shù)等, 假若String不是固定不變的,將會引起各種安全隱患。
6. String的不可變有什么好處
String不可變的好處,其實上面的String為什么要設(shè)計成不可變對象已經(jīng)用了一些代碼進行解答了,這里就整體歸納一下:
- 只有當字符串是不可變的,字符串池才有可能實現(xiàn)。字符串池的實現(xiàn)可以在運行時節(jié)約很多heap空間,因為不同的字符串變量都指向池中的同一個字符串。
- 如果字符串是可變的,那么會引起很嚴重的安全問題。比如,數(shù)據(jù)庫的用戶名、密碼都是以字符串的形式傳入來獲得數(shù)據(jù)庫的連接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。因為字符串是不可變的,所以它的值是不可改變的,否則黑客們可以鉆到空子,改變字符串指向的對象的值,造成安全漏洞。
- 因為字符串是不可變的,所以是多線程安全的,同一個字符串實例可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字符串自己便是線程安全的。
- 類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。比如你想加載java.sql.Connection類,而這個值被改成了myhacked.Connection,那么會對你的數(shù)據(jù)庫造成不可知的破壞。
- 因為字符串是不可變的,所以在它創(chuàng)建的時候hashcode就被緩存了,不需要重新計算。這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。
目前全部文章列表:
idea整合restful風格的ssm框架(一)
idea整合restful風格的ssm框架(二)
idea整合spring boot+spring mvc+mybatis框架
idea整合springboot+redis
JVM學習之—Java內(nèi)存區(qū)域
JVM學習之—垃圾回收與內(nèi)存分配策略
專題整理之—不可變對象與String的不可變
專題整理之—String的字符串常量池

