什么叫不可變類?
《Effective Java》將不可變類定義如下:
An immutable class is simply a class whose instances cannot be modified. All of the information contained in each instance is provided when it is created and is fixed for the lifetime of the object.
翻譯過來就是:
不可變類只是其實(shí)例不能被修改的類沉颂,每個(gè)實(shí)例中包含的所有信息都必須在創(chuàng)建該實(shí)例的時(shí)候就提供,并在對(duì)象的整個(gè)生命周期內(nèi)固定不變祝峻。
如何創(chuàng)建一個(gè)不可變類京髓?
要?jiǎng)?chuàng)建不可變類斩启,只要遵循下面幾條規(guī)則:
- 不要提供任何會(huì)修改對(duì)象狀態(tài)的方法。
- 保證類不會(huì)被拓展(一般聲明為final即可)芳誓。
- 使所有的域都是 private final的余舶。
- 確保對(duì)于任何可變組件的互斥訪問(可以理解如果中存在可變對(duì)象的域,得確鼻绿剩客戶端無法獲得其引用匿值,并且不要使用客戶端提供的對(duì)象來初始化這樣的域)。
Java出于安全性等因素考慮將某些類如:String赂摆、基本類型的包裝類挟憔、BigInteger钟些、BigDecimal設(shè)計(jì)成為不可變類。
不可變類真的不能改變嗎绊谭?
Java中的這些不可變類真的不能改變嗎政恍?我們不妨看下面的例子:
import java.lang.reflect.Field;
public class ChangeDemo {
public static void change(String str) {
if (null == str)
return;
try {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
char[] value_str = (char[]) f.get(str);
value_str[0] = 'h';
} catch (Exception e) {
e.printStackTrace();
}
}
public static void change(Integer num) {
if (null == num)
return;
try {
Field f = Integer.class.getDeclaredField("value");
f.setAccessible(true);
int value = (int) f.get(num);
f.set(num, value * 10);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
String a = "cat";
System.out.println("before:a=" + a);
change(a);
System.out.println("after :a=" + a);
Integer num = 12;
System.out.println("before:num=" + num);
change(num);
System.out.println("after :num=" + num);
}
}
輸出:
before:a=cat
after :a=hat
before:num=12
after :num=120
我們看到String、Integer這些不可變類都被改變了达传。但是不推薦大家這么干篙耗,這樣會(huì)帶來極大的安全隱患。
暴力反射修改不可變類帶來的安全隱患
例如:
import java.lang.reflect.Field;
public class ChangeDemo {
public static void change(String str) {
if (null == str)
return;
try {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
char[] value_str = (char[]) f.get(str);
value_str[0] = 'h';
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
String a = "cat";
String b = "cat";
System.out.println("before:a=" + a + ",b=" + b);
change(a);
System.out.println("after :a=" + a + ",b=" + b);
}
}
運(yùn)行結(jié)果如下:
before:a=cat,b=cat
after :a=hat,b=hat
我們驚奇地發(fā)現(xiàn)雖然我們只希望改變a的值趟大,但是b的值居然也被改變了鹤树。了解Java運(yùn)行時(shí)內(nèi)存結(jié)構(gòu)的朋友應(yīng)該知道铣焊,main方法中的a,b本身保存在main方法對(duì)應(yīng)的棧幀的局部變量表中逊朽,它們是方法區(qū)常量池中"cat"的引用,change方法中的局部變量通過引用使用暴力反射改變了常量池中“cat”的內(nèi)容曲伊,這直接導(dǎo)致了“cat”所有的引用的都帶來了安全隱患叽讳。
當(dāng)然,這個(gè)例子比較明顯坟募,那下面的例子您能夠看出問題嗎岛蚤?
import java.lang.reflect.Field;
public class UnsafeChangeDemo {
public static void main(String[] args)
throws Exception {
Integer a = 1;
Integer b = 2;
System.out.println("before:a = " + a + ", b = " + b);
swap(a, b);
System.out.println(" after :a = " + a + ", b = " + b);
}
public static void swap(Integer a, Integer b) throws Exception {
Field valueField = Integer.class.getDeclaredField("value");
valueField.setAccessible(true);
int aValue = (int) valueField.get(a);
int bValue = (int) valueField.get(b);
valueField.set(a, bValue);
valueField.set(b, aValue);
}
}
咋一看,這很簡單啊懈糯,不過就是通過暴力反射交換Integer的值而言啊涤妒,真的這么簡單嗎?請(qǐng)看運(yùn)行結(jié)果
before:a = 1, b = 2
after :a = 2, b = 2
嗯赚哗?有沒有搞錯(cuò)她紫,怎么交換后a,b全部變成2了?問題在哪里屿储?
我們來分析一下:
我們都知道贿讹,JDK1.5后提供了自動(dòng)裝拆箱的功能,其中Integer的自動(dòng)裝箱是通過調(diào)用Integer.valueOf(int)方法來實(shí)現(xiàn)的够掠,我們不妨看一下源碼:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];//即為 IntegerCache.cache[i + 128 ];
return new Integer(i);
}
在swap方法中民褂,先通過Field的 get(Object)方法獲取了a,b的值,然后通set(Object,Object)方法交叉賦值以達(dá)到交換a,b的目的疯潭。
但是set方法的參數(shù)是兩個(gè)Object赊堪,所以這里將會(huì)觸發(fā)自動(dòng)裝箱,了解Integer的朋友都知道(如果不清楚可以參考博主的博客Java基本數(shù)據(jù)類型及其封裝器的一些千絲萬縷的糾葛 )Integer內(nèi)部默認(rèn)提供了一個(gè)緩存數(shù)組static final Integer cache[256];來緩存-128~127的Integer對(duì)象竖哩。所以swap方法其中語句其實(shí)是這樣的:
public static void swap(Integer a, Integer b) throws Exception {
Field valueField = Integer.class.getDeclaredField("value");
valueField.setAccessible(true);
int aValue = (int) valueField.get(a);//1
int bValue = (int) valueField.get(b);//2
// 自動(dòng)裝箱 valueField.set(a, Integer.valueOf(2) );
//即 valueField.set(a, IntegerCache.cache[130] );
valueField.set(a, bValue);
// 自動(dòng)裝箱 valueField.set(b, Integer.valueOf(1));
//即valueField.set(b, IntegerCache.cache[129] );
valueField.set(b, aValue);
}
調(diào)用valueField.set(a, bValue);方法哭廉,將2賦給了a,由于a是IntegerCache.cache[129]的一個(gè)引用,所以實(shí)際上是將2賦值給了IntegerCache.cache[129]期丰。因此經(jīng)過這個(gè)方法調(diào)用后群叶,a由1變成了2吃挑,同時(shí),IntegerCache.cache[129]也由1變成了2 =至ⅰ舶衬!
在調(diào)用valueField.set(b, aValue)時(shí),本希望將1賦給b赎离,但由于自動(dòng)裝箱逛犹,其實(shí)上是將IntegerCache.cache[129] 的值賦值給b,然而IntegerCache.cache[129] 的值已經(jīng)由1變成了2,所以梁剔,b仍然等于2K浠!荣病!交換失斅胱!个盆!
好吧脖岛,一圖勝千言,程序的運(yùn)行時(shí)內(nèi)存結(jié)構(gòu)圖如下:
完整的解析如下:
public static void main(String[] args) throws Exception {
Integer a = 1;// 自動(dòng)裝箱 Integer.valueOf(1);
//即 IntegerCache.cache[129]
Integer b = 2;// 自動(dòng)裝箱 Integer.valueOf(2);
//即 IntegerCache.cache[130]
swap(a, b);
System.out.println(a + "," + b);
}
public static void swap(Integer a, Integer b) throws Exception {
Field valueField = Integer.class.getDeclaredField("value");
valueField.setAccessible(true);
int aValue = (int) valueField.get(a);//1
int bValue = (int) valueField.get(b);//2
// 自動(dòng)裝箱 valueField.set(a, Integer.valueOf(2) );
// 即 valueField.set(a, IntegerCache.cache[130]);
valueField.set(a, bValue);
//a 的值被修改為 2 實(shí)際上是 IntegerCache.cache[129]的值被修改為 2
// 再次自動(dòng)裝箱 valueField.set(b, Integer.valueOf(1));
// 即valueField.set(b, IntegerCache.cache[129] );
valueField.set(b, aValue);
//注意颊亮,這里的 IntegerCache.cache[129] 已經(jīng)被修改為 2了2癜稹!
//所以 valueField.set(b, aValue); 相當(dāng)于是 valueField.set(b, new Integer(2));
//所以最后a,b都為2V栈蟆绍在!
}
我的天,是不是感覺很繞呢雹有?所以不到萬不得已偿渡,一定不要通過暴力反射來修改不可變類的狀態(tài)!件舵!