項目15:最小化變動性
不變性類別(immutable class)的實例是無法修改的,所有實例的資訊是在產生時就提供的,在它的物件的生命週期中是固定的。不變性類別更容易設計、實作跟使用,減少出錯而且更安全。
建立不變性類別的五個準則:
- 不提供任何可以變更物件狀態的方法(mutators)。
- 確保類別不能被繼承。
- 所有屬性設為final:在沒有同步的清況下,新建的實例傳到另一個執行緒的時候,如果有一個參考指向它,需確保行為的正確性。
- 所有屬性設為private:避免用戶直接存取該屬性。
- 每個可變的元素,都有專門的存取方式:確保用戶無法取得類別中任何指向可變性物件的屬性的參考(reference),在建構子、存取方法使用防禦複制(defensive copy)。
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
// Accessors with no corresponding mutators
public double realPart() { return re; }
public double imaginaryPart() { return im; }
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex subtract(Complex c) {
return new Complex(re - c.re, im - c.im);
}
public Complex multiply(Complex c) {
return new Complex(re * c.re - im * c.im,
re * c.im + im * c.re);
}
public Complex divide(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp,
(im * c.re - re * c.im) / tmp);
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;
// See page 43 to find out why we use compare instead of == return Double.compare(re, c.re) == 0 &&
Double.compare(im, c.im) == 0;
}
@Override public int hashCode() {
int result = 17 + hashDouble(re);
result = 31 * result + hashDouble(im);
return result;
}
private int hashDouble(double val) {
long longBits = Double.doubleToLongBits(re);
return (int) (longBits ^ (longBits >>> 32));
}
@Override public String toString() {
return "(" + re + " + " + im + "i)";
}
}
功能方法(functional approach)模式:對運算的對象使用function,但不修改它,並將結果回傳。
- 不變性物件很簡單:不變性物件打從建立時,它的狀態就是固定的,假如你確保所有建構子保持類別的不變性,可減少所有使用該類別的程式設計人員的麻煩。
不變性物件有執行緒安全(thread-safe)的特性,不需同步:無疑是最簡單達到執行緒安全的方式。不變性物件應該利用此特性,讓用戶盡可能重複使用既存的實例。
public static final Complex ZERO = new Complex(0, 0); public static final Complex ONE = new Complex(1, 0); public static final Complex I = new Complex(0, 1);
進一步的做法是提供靜態工廠(static factory),將常用的實例cache起來,避免產生新的實例。 不變性物件不需要使用防禦複制,因為它的複制品是相同的實例,所以,這些不變性類別不應該提供clone方法和複制建構子(copy constructor)。
- 除了分享不變性物件,也可以共享其內部:例如,BigInteger類別使用sign-magnitude,符號由一個int表示,大小由一個int陣列呈現,BigInteger.negate()方法建立的實列就是指向同一個int陣列,而不是複制它
- Immutable objects make great building blocks for other objects
唯一的缺點是不同的值需要不同的物件:例如你有個百萬bit大小的BigInteger,而你只是想修改它的低位數
BigInteger moby = ...; moby = moby.flipBit(0);
flipBit()方法建立一個一樣百萬大小的BigInteger,但跟原本的物件只差一個bit。相反地,BitSet是可變性的,它提供一個方法用來更改單一位數的值。
假如你每一步都產生一個新的物件,那效能的問題是會被放大的,這裡有兩個解決的方法。首先,用基本型態(primitive)取代那些經常性的運算,那就不需要一直產生新的物件,比如提供一個public mutable companion class,像String就可以使用StirngBuilder,而BigSet對BigInteger來說就是這樣的角色。
另外,除了禁止繼承來維持immutable,還可以透過將建構子設為private或package-private,新增public static factory替代public建構子
// Immutable class with static factories instead of constructors
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) { this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
... // Remainder unchanged
}