如果一個對象的狀態在它構建之后就不能被更改,我們就認為它是不可更改的對象。對不可更改對象的最大限度的依賴被廣泛認為是一種建立簡單、可信賴代碼的好策略。
不可更改對象在并發的代碼中顯得格外有用。因為既然你不能更改它的狀態,那線程混淆和內存不一致都不可能出現了。
開發人員總是不太樂意使用不可更改對象,因為他們擔心創建一個新對象的代價會超過更新一個現有對象。對象新建的代價總是被過高估計,并且這個代價還可以被不可變對象的一些便利性所折中。包括減少垃圾收集的額外開銷,和本來需要的用來保護可變更對象的代碼。
接下來的一小節使用了一個類——它的實例是可更改的,然后從中衍生出一個有不可變實例的類。這樣給出了轉換的規則,也說明了不可變對象的幾個優勢。
一個同步類案例
SynchronizedRGB類,定義了顏色的對象。每個對象將顏色表示成為3個整數值,來表示三原色,以及一個字符串,表示該顏色的名字。
public class SynchronizedRGB {
// Values must be between 0 and 255.
private int red;
private int green;
private int blue;
private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public SynchronizedRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public void set(int red,
int green,
int blue,
String name) {
check(red, green, blue);
synchronized (this) {
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
}
public synchronized int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public synchronized String getName() {
return name;
}
public synchronized void invert() {
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
name = "Inverse of " + name;
}
}
SynchronizedRGB必須非常小心地使用,為了防止不連續的狀態被記錄下來。假設,一個線程執行了以下的代碼:
SynchronizedRGB color =
new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2
如果另一個線程在語句1之后,語句2之前,調用了color.set方法,mycolorInt的值就不會和mycolorName的值相匹配。為了避免出現這樣的結果,這兩句語句必須要被綁定在一起:
synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
這種不一致性只會出現在可變更的對象之上——它對不可變更的SynchronizedRGB來說,不是一個問題。
下面的幾條規則定義了一個創建不可變對象的簡單策略。不是所有被聲明為不可變的類都遵循以下的規則。這不一定說明那些類的發明人水平不行——他們可能有足夠的理由相信他們類的實例在構造之后不會改變。然而,這樣那樣的策略需要復雜的分析,對初學者太不友好。
- 不要提供setter方法——會更改域或對象的方法。
- 把所有的域聲明為私有和final。
- 不要允許子類重寫方法。實現這個最簡單的方式就是把該類聲明為final。一個更為復雜的方法,是把構造函數聲明為私有,然后在工廠方法中構建實例。
- 如果實例域包含對可變更對象的引用,不要允許這些對象發生改變。
4.1 不要提供能改變能變更對象的方法。
4.2 不要和可變更對象共享引用。永遠不要把外部可變的引用傳遞給構造器。 如果有必要,建立復制版本,然后把引用存儲在復制版本中。同樣的,創建你內部可變更對象的復制版本,來避免把原始的值直接返回給你的方法。
對SynchronizedRGB進行上面的策略,需要進行以下幾個步驟:
- 在該類中有兩個setter方法。第一個,set隨意地改變對象,在不可變類中沒有它的容身之地。第二個,invert,可以被改造成必須要新建一個新的對象,而不是在當前對象上進行修改。
- 所有的域已經都是私有了,它們需要進一步被聲明為final。
- 類本身要被聲明為final。
- 只有一個域引用了一個對象,而那個對象本身是不可變的。因此,來保證內部的可修改對象的安全衛士也不需要了。
經過這些修改之后,我們有了ImmutableRGB:
final public class ImmutableRGB {
// Values must be between 0 and 255.
final private int red;
final private int green;
final private int blue;
final private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public ImmutableRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public String getName() {
return name;
}
public ImmutableRGB invert() {
return new ImmutableRGB(255 - red,
255 - green,
255 - blue,
"Inverse of " + name);
}
}